Se você está aprendendo testes em Apex e, em algum momento, teve a sensação de que a Salesforce começou a jogar um monte de anotações aleatórias na sua frente, eu entendo.

De verdade.

Você abre uma classe de teste e aparece:

@IsTest
@TestSetup
@TestVisible

Aí, algumas linhas depois:

Test.startTest();
Test.stopTest();

Depois alguém fala de TestDataFactory, mock, System.runAs(), SeeAllData, cobertura de código...

E chega uma hora em que você pensa:

Calma. Eu só queria testar uma classe Apex. Em que momento isso virou uma discussão inteira de arquitetura?

Essa sensação é bem comum.

O problema é que testes em Apex muitas vezes são ensinados como uma lista de regras.

“Use essa anotação.”

“Crie esse método.”

“Chegue em 75% de cobertura.”

“Não use SeeAllData.”

E quando a gente aprende assim, tudo parece desconectado.

Então eu quero fazer diferente aqui.

Em vez de decorar anotação por anotação, vamos montar essa ideia juntos.

Porque, quando você entende por que cada uma dessas peças existe, testar Apex fica muito mais fácil de lembrar.

E, sinceramente, bem menos assustador.


Imagine que você acabou de terminar uma classe Apex.

Talvez algo simples.

Talvez algo assim:

public class AccountService {

    public static void activateAccount(Id accountId) {

        Account acc = [
            SELECT Id, Active__c
            FROM Account
            WHERE Id = :accountId
        ];

        acc.Active__c = true;

        update acc;
    }
}

Você olha e pensa:

Certo. Isso deve funcionar.

A Salesforce olha de volta e praticamente responde:

Ótimo. Agora prova.

E é aí que começam os testes.

Mas pensa comigo no que fazemos em seguida.

Criamos uma Account que não é real.

Preenchemos com dados fictícios.

Executamos o código real em cima desse registro falso.

Depois verificamos se o resultado ficou certo.

Ou seja, de certa forma, nós construímos um pequeno mundo falso para testar o mundo real.

Eu gosto de pensar em testes Apex dessa maneira porque várias coisas começam a fazer mais sentido.

O código de produção é a cidade real.

Os testes são o cenário de cinema.

Tudo parece real o suficiente para a cena acontecer, mas ninguém vai morar ali.

E agora podemos começar a falar de @IsTest.


“Tá, @IsTest quer dizer que essa é uma classe de teste, certo?”

Sim.

Mas tem um detalhe que surpreende muita gente.

Uma classe marcada com @IsTest não precisa necessariamente ter um método de teste executável.

Vale ler isso de novo porque é justamente aqui que muita confusão começa.

Você pode ter algo assim:

@IsTest
public class TestUtils {

    public static Account createAccount() {

        Account acc = new Account(
            Name = 'Test Account'
        );

        insert acc;

        return acc;
    }
}

E talvez sua primeira reação seja:

Mas cadê o teste?

Não tem.

E está tudo certo.

Essa classe não é responsável por executar um cenário de teste.

Ela é responsável por ajudar outro teste a montar o cenário.

Pensa novamente no cenário de cinema.

O ator é o método de teste.

A TestUtils é a equipe que monta a sala antes do ator entrar.

A equipe não é a cena.

Mas sem ela, talvez a cena nem aconteça.

É essa a lógica.


Agora olha um teste comum, sem helper nenhum.

@IsTest
private class AccountServiceTest {

    @IsTest
    static void shouldActivateAccount() {

        Account acc = new Account(
            Name = 'Test Account'
        );

        insert acc;

        AccountService.activateAccount(acc.Id);

        Account result = [
            SELECT Active__c
            FROM Account
            WHERE Id = :acc.Id
        ];

        System.assertEquals(
            true,
            result.Active__c
        );
    }
}

Não tem nada errado aqui.

Na verdade, para um projeto pequeno, está perfeitamente bom.

Mas agora imagina que esse não é seu único teste.

Tem outro que precisa de Account.

E outro.

E outro.

Depois Contacts.

Depois Opportunities.

Depois Products.

Depois Users.

Daqui a pouco, aquele mesmo código começa a aparecer em todo lugar:

Account acc = new Account(
    Name = 'Test Account'
);

insert acc;

E você pensa:

Ah, são só três linhas.

Até alguém mudar alguma coisa.

Um campo obrigatório novo.

Uma Validation Rule.

Uma nova regra de negócio.

E, de repente, vinte classes de teste quebram pelo mesmo motivo.

Aquelas três linhas já não parecem tão pequenas.

É aí que uma TestDataFactory começa a fazer muito sentido.


Por que os desenvolvedores criam uma Test Data Factory

Em vez de ensinar cada teste a criar uma Account válida, podemos colocar essa responsabilidade em um lugar só.

Por exemplo:

@IsTest
public class TestDataFactory {

    public static Account createAccount(
        String name
    ) {

        Account acc = new Account(
            Name = name
        );

        insert acc;

        return acc;
    }
}

Agora o teste pode simplesmente dizer:

Account acc =
    TestDataFactory.createAccount(
        'Acme Test'
    );

Fica mais limpo.

Mas, mais importante do que isso, a regra de criação daquela Account de teste agora está concentrada em um ponto.

Se amanhã alguma coisa mudar, você sabe onde procurar.

E é aqui que @IsTest no nível da classe fica realmente interessante.

Você está dizendo para a Salesforce:

Esse código existe apenas para apoiar testes.

Isso é diferente de código normal de produção.

E essa separação é intencional.

Código de teste não faz parte do comportamento real da aplicação.

Ele faz parte da estrutura usada para validar esse comportamento.


Mas existe uma pequena armadilha aqui.

E se você já trabalhou em uma org grande, talvez tenha visto isso.

Alguém cria:

TestUtils

Depois adiciona:

createAccount()

Depois:

createContact()
createOpportunity()
createCase()
createProduct()
createOrder()
createQuote()
createUser()
createContract()

Seis meses depois, a TestUtils tem 3 mil linhas.

E praticamente todos os testes dependem dela.

A classe criada para reduzir repetição virou uma dependência gigantesca.

Então sim, reutilização é boa.

Mas uma classe universal para a org inteira nem sempre é uma boa ideia.

Às vezes é bem mais saudável ter:

AccountTestFactory
ContactTestFactory
OpportunityTestFactory
UserTestFactory

Mesma ideia.

Menos caos.

Essa é uma parte que eu gostaria que explicassem com mais frequência quando falam de Test Data Factory.

O objetivo não é:

Coloque tudo dentro de uma TestUtils.

O objetivo é:

Pare de repetir setup de teste em todo lugar, mas mantenha sua arquitetura de testes sustentável.


“Espera. Se eu tenho uma Factory, por que preciso de @TestSetup?”

Essa pergunta faz todo sentido.

Porque os dois parecem resolver a mesma coisa.

Repetição de setup.

Mas eles resolvem isso em lugares diferentes.

Olha este exemplo:

@IsTest
private class AccountServiceTest {

    @TestSetup
    static void makeData() {

        Account acc = new Account(
            Name = 'Setup Account'
        );

        insert acc;
    }

    @IsTest
    static void shouldActivateAccount() {

        Account acc = [
            SELECT Id
            FROM Account
            LIMIT 1
        ];

        // teste
    }

    @IsTest
    static void shouldHandleAnotherScenario() {

        Account acc = [
            SELECT Id
            FROM Account
            LIMIT 1
        ];

        // outro teste
    }
}

Os dois métodos de teste precisam de uma Account.

Em vez de criar a mesma Account duas vezes, preparamos esse dado em @TestSetup.

A forma mais fácil que eu conheço de lembrar a diferença é esta:

Test Data Factory ajuda diferentes classes de teste a reutilizar a lógica de criação de dados.

@TestSetup ajuda métodos dentro da mesma classe de teste a compartilhar uma preparação comum.

É só isso.

Eles não são rivais.

Na verdade, trabalham muito bem juntos.

Você pode fazer:

@TestSetup
static void makeData() {

    TestDataFactory.createAccount(
        'Setup Account'
    );
}

Agora a Factory sabe como criar a Account.

E o @TestSetup decide quando aquela classe de teste precisa dessa Account.

Quando você enxerga dessa forma, fica bem mais simples.


Agora vamos falar de @TestVisible, porque aqui a confusão é outra

Tudo o que vimos até agora pertence ao universo de testes.

TestUtils.

Factories.

@TestSetup.

Agora imagina esta classe de produção:

public class PriceCalculator {

    private static Decimal applyDiscount(
        Decimal price,
        Decimal percentage
    ) {

        return price -
            (price * percentage);
    }
}

O método é private.

Então outra classe não pode acessá-lo normalmente.

Mas talvez o seu teste realmente precise acessar esse membro diretamente.

O Apex oferece:

@TestVisible

Então podemos escrever:

public class PriceCalculator {

    @TestVisible
    private static Decimal applyDiscount(
        Decimal price,
        Decimal percentage
    ) {

        return price -
            (price * percentage);
    }
}

Agora o teste consegue acessar o método.

Mas guarda uma coisa importante.

Esse método continua sendo código de produção.

@TestVisible não move o método para o universo de testes.

Ele apenas dá aos testes uma permissão especial de acesso.

Pensa numa porta de bastidores.

O público não entra.

A equipe técnica tem um crachá.

Só isso.


E aqui eu quero que você fique um pouco desconfiado.

Não do @TestVisible em si.

Ele é um recurso válido.

Fique desconfiado quando você começar a precisar dele em todo lugar.

Imagina encontrar isso dentro de uma classe de produção:

public class AccountController {

    @TestVisible
    private static Account createFakeAccountForTests() {

        Account acc = new Account(
            Name = 'Fake'
        );

        insert acc;

        return acc;
    }
}

Olha o nome:

createFakeAccountForTests

Esse método existe só porque o teste precisa de dados.

Então por que ele está dentro do código de produção?

Provavelmente este lugar faria mais sentido:

@IsTest
public class AccountTestFactory {
}

Uma pergunta ajuda bastante:

Se meus testes desaparecessem amanhã, esse método ainda teria uma razão real de negócio para existir?

Se a resposta for não, provavelmente ele pertence ao código de teste.

É uma pergunta simples.

Mas muito útil.


“Mas e se eu simplesmente quiser testar um método privado?”

Você pode usar @TestVisible.

Mas antes, faz outra pergunta.

Eu realmente preciso testar esse método privado diretamente?

Olha:

public class PriceCalculator {

    public static Decimal calculateFinalPrice(
        Decimal price
    ) {

        return applyDiscount(
            price,
            0.10
        );
    }

    private static Decimal applyDiscount(
        Decimal price,
        Decimal percentage
    ) {

        return price -
            (price * percentage);
    }
}

Você poderia testar apenas:

Decimal result =
    PriceCalculator.calculateFinalPrice(
        100
    );

System.assertEquals(
    90,
    result
);

Você não chamou applyDiscount() diretamente.

Mas o comportamento dele foi testado por meio do método público.

E isso muitas vezes é melhor.

Por quê?

Porque amanhã você pode mudar completamente a implementação privada.

Talvez applyDiscount() desapareça.

Talvez você crie um DiscountService.

Talvez o cálculo vá para outro lugar.

Se o resultado público continuar correto, seu teste provavelmente deveria continuar passando.

Essa é uma diferença importante entre testar comportamento e testar detalhe de implementação.

E, em projetos grandes, essa pequena diferença pesa bastante.


Agora temos dados. Temos helpers. Temos o cenário. Hora de executar o código real.

É aqui que entram:

Test.startTest();
Test.stopTest();

Se você é iniciante em testes Apex, talvez já tenha copiado essas linhas várias vezes sem entender muito bem o motivo.

Eu não quero deixar isso no nível de:

Só coloca isso em volta do método.

Existe uma razão real.

Imagina que você precisa criar bastante coisa antes do teste começar de verdade.

Talvez:

Account
Contacts
Product
Pricebook
Opportunity
User

Tudo isso usa recursos da Salesforce.

Queries acontecem.

DML acontece.

Governor Limits estão sendo consumidos.

Mas esse não é exatamente o código que você quer observar.

É apenas preparação.

Então podemos organizar assim:

Account acc =
    TestDataFactory.createAccount(
        'Test Account'
    );

Test.startTest();

AccountService.activateAccount(
    acc.Id
);

Test.stopTest();

Agora temos uma linha muito clara.

Antes de startTest():

Estou preparando o mundo.

Entre startTest() e stopTest():

Este é o comportamento que eu realmente quero avaliar.

Depois de stopTest():

Agora eu verifico o que aconteceu.

Essa é uma forma muito mais fácil de lembrar.


Talvez você também conheça o padrão:

Arrange
Act
Assert

Em Apex, ele costuma aparecer assim:

@IsTest
static void shouldActivateAccount() {

    // Arrange
    Account acc =
        TestDataFactory.createAccount(
            'Inactive Account'
        );

    // Act
    Test.startTest();

    AccountService.activateAccount(
        acc.Id
    );

    Test.stopTest();

    // Assert
    Account result = [
        SELECT Active__c
        FROM Account
        WHERE Id = :acc.Id
    ];

    System.assertEquals(
        true,
        result.Active__c
    );
}

Agora o teste parece uma pequena história.

E testes bons normalmente parecem.


Test.stopTest() fica ainda mais importante quando existe Apex assíncrono

Imagina que seu código faz isso:

System.enqueueJob(
    new AccountProcessor(
        accountId
    )
);

Esse Queueable é assíncrono.

Em uma execução normal, ele roda depois.

Mas seu teste não pode simplesmente dizer:

Beleza, talvez esse Queueable termine algum dia.

Você precisa de um resultado.

Então:

Test.startTest();

System.enqueueJob(
    new AccountProcessor(
        acc.Id
    )
);

Test.stopTest();

Depois você consegue verificar:

Account result = [
    SELECT Processed__c
    FROM Account
    WHERE Id = :acc.Id
];

System.assertEquals(
    true,
    result.Processed__c
);

Eu gosto de pensar no Test.stopTest() aqui como o momento em que você diz para o ambiente de teste:

Certo. Finalize o trabalho assíncrono que eu preciso para este cenário. Quero ver o resultado.

Não é exatamente viagem no tempo.

Mas parece um pouquinho.

E é justamente isso que torna código assíncrono testável de forma controlada.


Aí entram as integrações. E aqui a coisa fica interessante.

Imagina que sua classe Apex chama uma API externa.

Algo assim:

Http http = new Http();

HttpResponse response =
    http.send(request);

Agora pensa no que aconteceria se cada teste chamasse a API real.

A API caiu hoje?

Seu teste falha.

A senha mudou?

Seu teste falha.

O time externo mudou o retorno?

Seu teste falha.

O endpoint ficou lento?

Seu teste fica lento.

E nada disso significa necessariamente que seu Apex está errado.

Então fazemos algo mais inteligente.

Simulamos o sistema externo.

Por exemplo:

@IsTest
global class CustomerApiMock
    implements HttpCalloutMock {

    global HTTPResponse respond(
        HTTPRequest request
    ) {

        HttpResponse response =
            new HttpResponse();

        response.setStatusCode(200);

        response.setBody(
            '{"status":"ok"}'
        );

        return response;
    }
}

Depois:

Test.setMock(
    HttpCalloutMock.class,
    new CustomerApiMock()
);

Agora seu código acredita que recebeu uma resposta HTTP.

Mas nenhuma conexão real com a internet foi necessária.

Construímos uma internet falsa dentro do teste.

E isso é muito poderoso.

Porque agora você decide o que essa internet faz.

Hoje:

200 OK

No próximo teste:

401 Unauthorized

Depois:

404 Not Found

Depois:

500 Internal Server Error

Outro com JSON quebrado.

Outro com campo faltando.

E isso importa porque sua integração não existe apenas quando tudo funciona lindamente.

O caminho de erro também faz parte do comportamento.

Testar apenas HTTP 200 é como testar um guarda-chuva só quando está fazendo sol.

Prova bem pouco.


E quando o comportamento muda de acordo com o usuário?

É aqui que aparece System.runAs().

Talvez seu código se comporte de forma diferente dependendo de quem está executando.

Talvez o acesso a registros importe.

Talvez sharing importe.

Nesse caso, você pode criar um usuário e executar parte do teste assim:

System.runAs(restrictedUser) {

    List<Account> accounts = [
        SELECT Id
        FROM Account
    ];

    // asserts
}

Agora o código roda sob outro contexto de usuário.

Isso é útil, especialmente em cenários ligados a record sharing.

Mas quero parar aqui por um segundo porque esse é outro ponto que alguns materiais simplificam demais.

System.runAs() não significa:

Testar automaticamente toda a segurança da Salesforce.

Sharing, CRUD, FLS, contexto de usuário e mecanismos de enforcement estão relacionados, mas não são exatamente a mesma coisa.

Então pergunte:

O que eu realmente estou tentando provar neste teste?

Se a resposta envolve acesso a registros ou comportamento sob outro usuário, runAs() pode ser exatamente o que você precisa.

Se a resposta envolve Field-Level Security, a conversa já é outra.

Não dê a uma ferramenta responsabilidades que ela não tem.


Nesse ponto talvez você esteja pensando: nossa, isso é muito maior do que “escrever uma classe de teste”

Exatamente.

Olha o que já apareceu:

@IsTest
TestUtils
TestDataFactory
@TestSetup
@TestVisible
Test.startTest()
Test.stopTest()
HttpCalloutMock
Test.setMock()
System.runAs()

Isso não é apenas “código para coverage”.

Estamos construindo um ambiente controlado no qual conseguimos:

criar dados,

simular usuários,

simular APIs,

controlar o contexto de execução,

provocar erros,

testar processamento assíncrono,

e validar resultados.

É por isso que eu gosto tanto da ideia do universo paralelo.

A aplicação real está em um mundo.

Os testes constroem outro mundo ao redor dela só para perguntar:

Você realmente está fazendo aquilo que deveria?


E agora precisamos falar do número que todo mundo lembra: 75%

Se você trabalha com Salesforce, cedo ou tarde escuta:

Apex precisa de 75% de cobertura.

E sim, requisitos de cobertura importam em deployments.

Mas aqui muitos times cometem um erro.

Eles começam a escrever testes para o número.

Não para o comportamento.

Olha:

@IsTest
static void coverageTest() {

    Account acc = new Account(
        Name = 'Test'
    );

    insert acc;

    AccountService.activateAccount(
        acc.Id
    );
}

Talvez isso execute várias linhas.

A cobertura sobe.

Ótimo.

Mas o que provamos?

A Account ficou ativa?

Não sabemos.

O método atualizou o registro certo?

Não sabemos.

Alguma outra coisa quebrou?

Também não sabemos.

O teste apenas executou código.

Agora compara com:

Account result = [
    SELECT Active__c
    FROM Account
    WHERE Id = :acc.Id
];

System.assertEquals(
    true,
    result.Active__c
);

Agora estamos dizendo alguma coisa.

Estamos dizendo:

Depois dessa ação, espero este resultado.

Isso é teste.

Coverage importa.

Mas coverage é consequência de executar cenários que fazem sentido.

Não deveria ser o objetivo inteiro do teste.


Eu gosto de explicar coverage com uma pergunta

Coverage pergunta:

O teste passou por essa linha?

Ele não pergunta automaticamente:

O resultado dessa linha estava correto?

Essa é a função da sua asserção.

Imagina:

if (customerType == 'Premium') {

    applyPremiumDiscount();

} else {

    applyStandardDiscount();
}

Talvez você cubra os dois caminhos.

Ótimo.

Mas o desconto Premium estava correto?

E o Standard?

Essas são outras perguntas.

Por isso até 100% de cobertura não significa automaticamente excelentes testes.

Você pode executar tudo e validar quase nada.


“Então o que eu deveria testar de verdade?”

Depende da regra de negócio.

Não existe uma quantidade universal de métodos de teste.

Mas eu geralmente começo fazendo algumas perguntas.

O que acontece quando tudo está certo?

Esse é o happy path.

Depois:

O que acontece quando a entrada está errada?

Talvez null.

Talvez dado ausente.

Talvez um status inválido.

Depois:

Esse código pode receber muitos registros?

Se sim, pense em bulk.

Depois:

Ele chama outro sistema?

Teste sucesso e falha.

Depois:

O contexto de usuário importa?

Talvez precise de runAs().

Depois:

Existe processamento assíncrono?

Agora startTest() e stopTest() ficam importantes.

Percebe o que mudou?

A pergunta deixou de ser:

Como eu chego em 75%?

E virou:

O que pode acontecer na vida real?

A cobertura normalmente vem junto.


E precisamos conversar sobre SeeAllData=true

Porque ele é tentador.

Muito tentador.

Você encontra:

@IsTest(SeeAllData=true)

e descobre que o teste pode enxergar dados que já existem na org.

E a cabeça pensa:

Espera aí. Então por que eu estou criando todas essas Accounts falsas?

Pergunta justa.

A org já tem Accounts.

Contacts.

Products.

Users.

Por que não usar?

Porque agora seu teste depende de um mundo que você não controla.

Imagina:

Account acc = [
    SELECT Id
    FROM Account
    WHERE Name = 'Premium Customer'
    LIMIT 1
];

Hoje essa Account existe.

O teste passa.

Amanhã alguém muda o nome.

O teste falha.

Nada mudou na sua classe Apex.

Nada mudou na regra de negócio.

O teste falhou porque o ambiente mudou.

Não é esse tipo de confiança que queremos de um teste automatizado.

Lembra do cenário de cinema?

Se a cena precisa de um carro vermelho, você não diz:

Tomara que amanhã tenha um carro vermelho estacionado perto do estúdio.

Você leva o carro.

Mesma lógica.

Crie os dados de que seu teste precisa.

Controle o cenário.

Deixe o resultado previsível.


Um bom teste também deveria parecer uma conversa

Essa é uma coisa que eu acho que muita gente ignora.

Código de teste também é código que alguém vai ler.

Talvez você leia daqui a seis meses.

Talvez outro desenvolvedor.

Talvez alguém esteja investigando um problema em produção e abra seu teste para entender qual deveria ser o comportamento.

Olha estes nomes:

test1()
test2()
testMethod()

Não dizem nada.

Agora olha:

shouldActivateInactiveAccount()

ou:

shouldThrowExceptionWhenAccountDoesNotExist()

Na hora você entende alguma coisa sobre o sistema.

Testes bons também são documentação.

Eles contam para o próximo desenvolvedor:

É isso que esperamos que aconteça.

E, sinceramente, seu “eu do futuro” também vai agradecer.


Não teste apenas quando tudo está bonito

Sistemas reais nem sempre estão bonitos.

Às vezes a Account não existe.

Às vezes a API responde 500.

Às vezes o valor vem null.

Às vezes a lista está vazia.

Às vezes chegam 200 registros de uma vez.

Às vezes o usuário não pode acessar aquele registro.

Às vezes o job assíncrono falha.

Esses cenários importam.

Se seu código tem:

if (accountId == null) {

    throw new IllegalArgumentException(
        'Account Id is required.'
    );
}

esse comportamento também merece um teste.

Porque a exceção não é um acidente.

Ela faz parte do contrato.

O sistema está dizendo:

Se você me passar uma entrada inválida, esta é a minha resposta.

Isso também é comportamento.

E comportamento é o que testamos.


Bulk testing é aquele momento em que o Apex lembra que estamos no Salesforce

Esse ponto importa muito.

Código pode funcionar perfeitamente com um registro.

E falhar completamente com duzentos.

Por exemplo:

for (Account acc : accounts) {

    List<Contact> contacts = [
        SELECT Id
        FROM Contact
        WHERE AccountId = :acc.Id
    ];
}

Uma Account?

Parece tranquilo.

Muitas Accounts?

Agora temos um problema.

Então, quando seu código pode processar coleções, vale perguntar:

Isso funciona só no meu teste bonitinho com um registro?

Ou:

Continua funcionando quando a Salesforce manda volume de verdade?

Essa pergunta pode evitar alguns bugs bem dolorosos em produção.


E às vezes o teste é difícil porque o código de produção também é difícil

Essa é uma das minhas lições favoritas sobre testes.

Imagine que, para testar um comportamento simples, você precisa:

criar quinze registros,

alterar várias variáveis privadas,

usar @TestVisible em todo lugar,

mockar três dependências,

usar Test.isRunningTest() em produção,

e executar tudo numa ordem muito específica.

Em algum momento, vale perguntar:

Meu teste está difícil ou minha classe está fazendo coisa demais?

Talvez ela esteja:

consultando,

validando,

transformando,

salvando,

chamando API,

montando payload,

checando permissão,

e enfileirando job.

Tudo no mesmo lugar.

Claro que testar fica doloroso.

Testabilidade pode contar muita coisa sobre design.

Não sempre.

Mas muitas vezes.

Quando uma classe é muito difícil de testar, talvez ela esteja tentando dizer:

Estou com responsabilidades demais.

Vale escutar.


Código de teste também pode virar dívida técnica

Sim.

Teste também.

Você pode abrir um projeto antigo e encontrar:

SeeAllData=true em todo lugar

Uma TestUtils gigante.

Métodos chamados test1, test2, test3.

Mocks que só sabem responder 200.

@TestVisible em metade da classe.

Nenhum cenário de bulk.

Nenhuma asserção realmente útil.

Setup copiado em vários lugares.

A cobertura pode até parecer bonita.

Mas ninguém confia nos testes.

E isso é um problema sério.

Porque o valor real de uma suíte de testes é confiança.

Você muda o código.

Os testes passam.

Você quer acreditar:

Ótimo. O comportamento importante continua funcionando.

Se a suíte é frágil ou vazia, essa confiança desaparece.


Então vamos juntar tudo

Se você vê:

@IsTest
public class TestUtils {
}

pense:

Essa classe pertence ao mundo dos testes.

Se vê:

@IsTest
static void shouldDoSomething() {
}

pense:

Esse é um cenário de teste de verdade.

Se vê:

@TestSetup

pense:

Esses são os dados iniciais comuns desta classe de teste.

Se vê:

@TestVisible

pense:

Isso continua sendo produção, mas o teste ganhou acesso especial.

Se vê:

Test.startTest();

pense:

A preparação acabou. Agora começa a ação principal.

Se vê:

Test.stopTest();

pense:

A ação terminou. Agora posso validar o resultado, inclusive o processamento assíncrono relevante.

Se vê:

Test.setMock(...)

pense:

Estamos trocando uma dependência externa real por uma versão controlada.

Se vê:

System.runAs(user)

pense:

Este cenário depende de outro contexto de usuário.

E se vê:

@IsTest(SeeAllData=true)

talvez pare por um segundo e pergunte:

Por que este teste precisa dos dados reais da org?

Não significa:

Isso está sempre errado.

Só significa:

Por quê?

Essa pergunta já evita muito teste frágil.


Antes de fechar a classe de teste, vale fazer algumas perguntas

Não precisa virar um checklist enorme.

É mais uma conversa rápida com o próprio teste.

O nome do método explica o que estou testando?

Eu controlo os dados?

Estou verificando o resultado real ou apenas executando linhas?

O que acontece quando a entrada está errada?

Esse código pode receber muitos registros?

O que acontece se o sistema externo falhar?

O contexto de usuário importa?

Existe processamento assíncrono?

Estou usando @TestVisible porque realmente preciso ou porque o design está brigando comigo?

Esse teste continuaria funcionando se alguém apagasse todos os dados normais da sandbox?

Essas perguntas são muito mais úteis do que:

Já cheguei em 75%?


E agora volta comigo para o começo

Você escreveu uma classe Apex.

A Salesforce pediu que você provasse que ela funciona.

Então você construiu outro mundo.

Um mundo com Accounts falsas.

Contacts falsas.

Opportunities falsas.

Respostas falsas de API.

Usuários temporários.

Erros provocados de propósito.

Jobs assíncronos controlados dentro do cenário.

Dados que desaparecem quando o teste termina.

Nenhum cliente vai ver esse mundo.

Nenhum usuário de negócio vai entrar no Salesforce e dizer:

Nossa, que Test Data Factory bonita.

Eles provavelmente nunca vão saber que isso existe.

Mas essa estrutura invisível te entrega algo muito importante.

Confiança.

Confiança para alterar uma classe.

Confiança para refatorar.

Confiança para fazer deploy.

Confiança de que, quando uma coisa mudou, outro comportamento importante não quebrou silenciosamente.

E essa é a parte que eu gostaria que alguém tivesse explicado antes de simplesmente dizer:

Salesforce exige 75% de coverage.

Porque sim, coverage importa.

Regras de deployment importam.

Mas testes são maiores do que isso.

Uma boa suíte de testes continua perguntando para sua aplicação:

Você ainda está fazendo aquilo que prometeu fazer?

E cada vez que a resposta é sim, você ganha um pouco mais de segurança para continuar evoluindo o sistema.

É por isso que testes Apex não são apenas código que escrevemos porque a Salesforce obriga.

Eles são a estrutura invisível atrás do código que todo mundo vê.

E quando essa estrutura invisível é forte, o restante da aplicação fica muito mais seguro de mudar.