Guia definitivo (e simples) sobre Classes de Teste em Apex

Para quem está chegando agora em Salesforce e até em programação: testes, cobertura e qualidade explicados com calma, analogias e exemplos práticos.

Por Monique da Silva · 23 de julho de 2026 · 36 min de leitura

Guia definitivo e simples sobre Classes de Teste em Apex para iniciantes

Para quem está chegando agora em Salesforce — e até em programação.

Se você tá lendo isso agora, é bem provável que alguém já tenha falado pra você "precisa criar a classe de teste" e você tenha ficado sem saber muito bem por onde começar. Ou talvez você já tenha até escrito uma, mas ficou aquela pulga atrás da orelha: "será que eu fiz isso do jeito certo, ou só fingi que fiz?"

Se for esse o seu caso, relaxa — é completamente normal, e você não está sozinho(a) nisso. A grande maioria do material técnico por aí explica esse assunto de um jeito seco, cheio de termo jogado sem explicação, como se todo mundo já soubesse do que se trata. Aqui a proposta é diferente: a gente vai com calma, com analogia, com exemplo de verdade, e sem cobrar de você já saber nada de antemão. Se em algum momento aparecer uma palavra estranha, ela vai ser explicada ali mesmo, sem rodeio.

Bora começar?


0. Antes de tudo: por que isso existe?

Já te aconteceu de mudar uma linha bem pequena, quase inofensiva, e no dia seguinte alguém aparecer dizendo que "algo parou de funcionar" — e você nem imaginava que aquele pedacinho de código tinha ligação com o resto? Se sim, seja bem-vindo(a) ao motivo de existir das classes de teste. E se ainda não te aconteceu... relaxa, mais cedo ou mais tarde acontece com todo mundo que programa. É extremamente normal, e é exatamente para isso que os testes existem.

Pensa numa fábrica por um instante. Nenhum produto sai da linha de produção direto pra prateleira do cliente. Antes, ele passa por um controle de qualidade — alguém (ou alguma máquina) confere se aquela peça está mesmo dentro do padrão esperado. Ninguém olha de longe pra uma peça e fala "ah, parece que tá certo, pode mandar".

Uma classe de teste é esse controle de qualidade, só que para o seu código.

Ela existe para responder uma pergunta simples, mas que muda tudo:

"Eu mudei alguma coisa aqui. Será que quebrei algo que já funcionava em outro lugar?"

Sem teste, essa resposta só aparece quando o usuário reclama — ou pior, quando o processo falha silenciosamente em produção, com dado real, sem ninguém perceber na hora. Com teste, a resposta aparece em segundos, no seu próprio ambiente, muito antes de qualquer pessoa real ser afetada.

Guarda esse ponto com carinho, porque ele é a bússola do guia inteiro: cobertura de código e a exigência dos 75% são só consequências desse objetivo. Elas não são o objetivo em si. Isso vai ficar bem claro conforme a gente avança.

Combinado? Então bora destravar os termos técnicos que vão aparecer no caminho, pra você não travar em nenhuma palavra estranha lá na frente.


1. Glossário rápido (vale a pena ler antes do resto)

Se você é novo em programação, um monte de termo técnico vai aparecer sem aviso na maioria dos materiais por aí — e isso é frustrante, porque te tira do fluxo de entendimento bem na hora em que você tava pegando o jeito. Aqui, não. Vamos deixar tudo traduzido antes de continuar:

Termo O que significa, em uma frase
Apex A linguagem de programação do Salesforce, parecida com Java.
Classe Um "arquivo" de código que agrupa um conjunto de comportamentos relacionados.
Método Um bloco de código dentro de uma classe que executa uma ação específica (equivalente a uma "função").
Trigger Um código que roda automaticamente quando um registro é criado, alterado ou excluído (ex: sempre que um Case é salvo).
SOQL A "linguagem de consulta" usada para buscar registros no banco de dados do Salesforce (parecida com SQL).
DML As operações que gravam dados: inserir, atualizar, excluir, fazer upsert.
Bulk / em massa Processar vários registros de uma vez (ex: 200 Contatos importados juntos), não só um por um.
Assíncrono Código que não roda na hora, e sim depois, em segundo plano (Future, Queueable, Batch, Scheduled).
Callout Uma chamada do Apex para um sistema externo (uma API fora do Salesforce).
Mock Uma "simulação" de algo (geralmente um callout) usada em teste, para não depender do sistema externo de verdade.
Governor limits Os "limites de recursos" que o Salesforce impõe ao seu código (quantas queries, quantos DMLs, etc. você pode fazer numa execução).

Guarda essa tabela. Se em algum momento você travar num termo lá na frente, é só voltar aqui — ninguém vai saber, prometo.

Anotou? Então bora ver, na prática, o que é de fato essa tal "classe de teste".


2. O que é, de fato, uma classe de teste?

Uma classe de teste é um código Apex que não entrega nenhuma funcionalidade direto pro usuário final — ela existe só para executar outra classe (a que resolve o problema de negócio de verdade) e conferir se o resultado está certo.

Analogia: a inspeção do prédio

Pensa assim:

  • A classe que você quer testar é o prédio.
  • Os métodos dela são os cômodos.
  • Quando você executa o teste, é como se um inspetor caminhasse por alguns desses cômodos.
  • A cobertura de código é o mapa térmico de quais cômodos o inspetor visitou.
  • O Assert é o inspetor de fato conferindo: "essa tomada funciona?", "essa porta tranca?", "esse encanamento não vaza?".

Repare numa coisa importante: e se esse inspetor andasse pelos cômodos todos... de olhos vendados? Ele passaria por todo lugar, o mapa mostraria 100% de cobertura, e mesmo assim nenhuma tomada teria sido testada de verdade. É exatamente isso que acontece quando um teste roda o código mas não usa Assert — ele gera cobertura, mas não prova nada. É a diferença entre "o inspetor esteve lá" e "o inspetor confirmou que está tudo em ordem".

Cobertura responde: "por onde o teste passou?" Assert responde: "o que aconteceu ali estava correto?"

Se você só for levar uma frase deste guia, leve essa.

Vendo isso na prática (o teste "fraco" vs. o teste de verdade)

Imagine esta classe, bem simples, que calcula o valor do frete de um pedido:

public with sharing class ShippingFeeService {

    public static Decimal calculate(Decimal orderTotal, Boolean premiumCustomer) {
        if (orderTotal == null || orderTotal < 0) {
            return null;
        }

        if (premiumCustomer == true || orderTotal >= 200) {
            return 0;
        }

        return 25;
    }
}

As regras de negócio são:

  1. Se o valor do pedido for nulo ou negativo → não faz sentido, retorna null.
  2. Se o cliente for premium → frete grátis.
  3. Se o pedido for R$ 200 ou mais → frete grátis.
  4. Qualquer outro caso → frete de R$ 25.

Agora, um teste fraco (o tipo que parece certo, mas não é):

@IsTest
static void testeFraco() {
    ShippingFeeService.calculate(100, false);
}

Esse teste roda a linha, gera cobertura... e não prova absolutamente nada. Se um dia alguém, sem querer, mudar o return 25 para return 2000 (foi só um dedo que escorregou no teclado), esse teste continua passando tranquilamente — porque ele nunca checou o valor retornado. E aí, o cliente pode acabar levando uma cobrança completamente fora do esperado no cartão, sem que ninguém tenha percebido nada de errado no painel de testes.

Agora o teste correto, que valida de verdade:

@IsTest
static void shouldChargeStandardFee() {
    Decimal actualFee = ShippingFeeService.calculate(100, false);

    Assert.areEqual(
        25,
        actualFee,
        'Um pedido comum abaixo de 200 deve pagar frete de 25.'
    );
}

A diferença entre os dois é uma linha: o Assert. É ela que transforma "eu rodei o código" em "eu provei que o código está certo".

Faz sentido até aqui? Então vamos ver como organizar esse método de teste de um jeito que qualquer pessoa do time consiga ler e entender rapidinho.


3. A anatomia de um método de teste: Arrange, Act, Assert

Todo método de teste bem escrito segue (mesmo sem dizer isso explicitamente) uma receita de 3 passos, chamada AAA:

Passo Pergunta que ele responde Na prática
Arrange (organizar) De quais dados eu preciso para testar isso? Criar variáveis, registros, cenário
Act (agir) Qual código estou testando de fato? Chamar o método que quero validar
Assert (confirmar) O resultado bateu com o que eu esperava? Comparar o resultado real com o esperado

Analogia: uma receita de bolo em laboratório

  1. Você separa os ingredientes (Arrange).
  2. Você assa o bolo (Act).
  3. Você prova um pedaço e confere se ficou doce, se cresceu, se não queimou (Assert).

Ninguém pula a etapa 3 e diz "o bolo está pronto" só porque o forno apitou. O apito do forno é a "cobertura" — ele só avisa que o tempo acabou. Provar o bolo é o "Assert" — a prova de gosto obrigatória.

Vendo isso no código:

@IsTest
static void shouldChargeStandardFee() {
    // Arrange: preparo o cenário
    Decimal orderTotal = 100;
    Boolean premiumCustomer = false;
    Decimal expectedFee = 25;

    // Act: executo o que quero testar
    Test.startTest();
    Decimal actualFee = ShippingFeeService.calculate(orderTotal, premiumCustomer);
    Test.stopTest();

    // Assert: confiro o resultado
    Assert.areEqual(
        expectedFee,
        actualFee,
        'Um pedido comum abaixo de 200 deve pagar frete de 25.'
    );
}

Sobre o Test.startTest() / Test.stopTest() — e por que eles existem de verdade

Muita gente enxerga esse par só como um "marcador visual", tipo um comentário chique dizendo "agora começa a parte que importa". Mas a explicação real vai um pouco além disso, e vale a pena entender — prometo que não é tão complicado quanto parece.

O Salesforce é um ambiente multitenant — ou seja, milhares de empresas diferentes dividem a mesma estrutura de servidores ao mesmo tempo. Pra isso funcionar sem uma empresa atrapalhar a outra, existe um sistema de "cotas": os governor limits, que controlam quantas consultas, quantas gravações, quantas chamadas você pode fazer dentro de uma única execução.

Só que a etapa de Arrange (criar todos os dados falsos pro teste) já consome uma parte dessas cotas. Se isso não fosse separado do resto, você corre o risco de o teste falhar não porque o código tem um bug, mas simplesmente porque a preparação do cenário consumiu limite demais.

É aí que entra o Test.startTest(): ele reinicia essas cotas do zero, garantindo que o método que você está testando de fato tenha a capacidade completa do sistema disponível pra ele — sem carregar nas costas o peso da preparação.

E o Test.stopTest() faz algo ainda mais importante: se o código testado disparar algo assíncrono (Future, Queueable, Batch, Scheduled Apex), é ele quem força esse processamento a rodar na hora, de forma controlada, ali mesmo dentro do teste — como se ele "congelasse o tempo", executasse tudo que ficou pendente, e só depois liberasse a passagem para o seu Assert. Sem isso, você poderia tentar validar um resultado que ainda nem tinha sido processado.

Por isso, cada método de teste só pode usar esse par uma vez.

Fonte oficial: Salesforce — Using Limits, startTest and stopTest

Tranquilo até aqui? Ótimo — porque agora a gente vai juntar todas essas peças numa classe de teste completa, pra você ter um modelo de verdade pra olhar sempre que precisar.


4. A classe de teste completa (o modelo a seguir)

Juntando tudo, uma classe de teste séria para o ShippingFeeService cobre todos os caminhos possíveis da regra de negócio — não só o caminho "feliz":

@IsTest
private with sharing class ShippingFeeServiceTest {

    @IsTest
    static void shouldChargeStandardFee() {
        Test.startTest();
        Decimal actualFee = ShippingFeeService.calculate(100, false);
        Test.stopTest();

        Assert.areEqual(
            25,
            actualFee,
            'Um pedido comum abaixo de 200 deve pagar frete.'
        );
    }

    @IsTest
    static void shouldGiveFreeShippingAtBoundary() {
        Test.startTest();
        Decimal actualFee = ShippingFeeService.calculate(200, false);
        Test.stopTest();

        Assert.areEqual(
            0,
            actualFee,
            'O valor exato de 200 deve conceder frete grátis (limite da regra).'
        );
    }

    @IsTest
    static void shouldGiveFreeShippingToPremiumCustomer() {
        Test.startTest();
        Decimal actualFee = ShippingFeeService.calculate(50, true);
        Test.stopTest();

        Assert.areEqual(
            0,
            actualFee,
            'Cliente premium deve ter frete grátis, mesmo em pedido pequeno.'
        );
    }

    @IsTest
    static void shouldReturnNullForInvalidTotal() {
        Test.startTest();
        Decimal actualFee = ShippingFeeService.calculate(null, false);
        Test.stopTest();

        Assert.isNull(
            actualFee,
            'Valor nulo é uma entrada inválida e deve retornar null.'
        );
    }

    @IsTest
    static void shouldReturnNullForNegativeTotal() {
        Test.startTest();
        Decimal actualFee = ShippingFeeService.calculate(-1, false);
        Test.stopTest();

        Assert.isNull(
            actualFee,
            'Valor negativo é uma entrada inválida e deve retornar null.'
        );
    }
}

Por que essa classe é um bom modelo?

Repara no que ela faz — isso é a boa prática, não é decoração:

  • Um método, um comportamento. Cada @IsTest testa uma única regra de negócio. Isso facilita muito saber exatamente o que quebrou quando um teste falha.
  • O nome do método já conta a história. shouldGiveFreeShippingToPremiumCustomer — você não precisa nem abrir o método pra saber o que ele valida.
  • Ela cobre cenário positivo e negativo. Entrada válida (100, 200, cliente premium) e entrada inválida (nulo, negativo).
  • Ela testa o valor de fronteira. 200 é exatamente o limite da regra (>= 200). Testar esse limite é onde a maioria dos bugs de "quase certo" mora.
  • Cada Assert vem com uma mensagem. Se o teste falhar, a mensagem explica o que deveria ter acontecido — muito mais rápido de debugar do que um "expected 0, got 25" seco.
  • Nenhum dado depende da organização. Não busca nenhum registro existente, não depende de nada que alguém possa ter apagado ou mudado.

Essa lista, aliás, é basicamente um resumo das próprias recomendações oficiais da Salesforce: testar entradas válidas e inválidas, testar cada ramo de lógica condicional (if/else), garantir que o teste rode sem lançar exceções não previstas, e usar a classe Assert para provar o comportamento — não presumir que "rodou sem erro" já é suficiente. (Salesforce — Testing Best Practices)

Show, com esse modelo guardado, vamos falar de um detalhe que trava muita gente iniciante: como lidar com registros de verdade (Account, Case, Contact...) sem bagunçar a organização.


5. E quando o código trabalha com registros (Account, Case, Contact...)?

Por padrão, um teste Apex não enxerga os dados reais da sua organização. E isso é proposital: um teste que depende de um registro específico existir pode passar numa sandbox e falhar em outra, ou parar de funcionar do nada porque alguém excluiu ou alterou aquele registro num dia qualquer.

A regra de ouro é: o teste cria os próprios dados. Sempre.

@IsTest
private class AccountExampleTest {

    @TestSetup
    static void createTestData() {
        insert new Account(Name = 'Conta criada pelo teste');
    }

    @IsTest
    static void shouldFindCreatedAccount() {
        Account accountRecord = [
            SELECT Id, Name
            FROM Account
            WHERE Name = 'Conta criada pelo teste'
            LIMIT 1
        ];

        Assert.areEqual('Conta criada pelo teste', accountRecord.Name);
    }
}

Como o @TestSetup protege um método do outro

Aqui mora uma dúvida bem comum de quem tá começando: "se eu tenho 8 métodos de teste na mesma classe, e o primeiro método deletar um registro criado pelo @TestSetup, isso não vai bagunçar os outros 7 métodos que rodam depois?"

Ótima pergunta — e a resposta é não, e o motivo é bonito de entender. O método marcado com @TestSetup roda uma única vez, isolado, e cria um estado "fotografado" no banco de dados de teste. É como tirar uma foto de alta resolução daquele cenário. A partir daí, cada método de teste da classe recebe a sua própria cópia independente dessa fotografia — eles não dividem a mesma instância real do dado.

Então, se o primeiro método deletar aquele registro pra testar uma rotina de exclusão, ao final da execução dele o banco desfaz (dá um "rollback") em qualquer alteração feita localmente, e entrega a base intacta e limpa para o próximo método da fila. Isso, aliás, vale pra qualquer dado inserido durante o teste, com ou sem @TestSetup: tudo é revertido automaticamente no final, e nada fica gravado de verdade na sua org.

Fonte oficial: Salesforce — Using Test Setup Methods

O que evitar: SeeAllData=true

@IsTest(SeeAllData=true)

Isso libera o teste para enxergar os dados reais da organização. Parece um atalho tentador — principalmente quando a criação de dados de teste está dando trabalho — mas é uma armadilha: o teste passa a depender de coisas fora do seu controle. Se a validação da sua regra só funciona porque existe um registro específico cadastrado na organização, o que acontece no dia em que alguém desativar ou editar exatamente esse registro, sem nem saber que um teste dependia dele? A suíte inteira quebra, e nem foi culpa de nenhuma mudança no código.

Evite SeeAllData=true como prática padrão, salvo em casos bem específicos e raros.

Quando os dados de teste começam a virar bagunça: o Test Data Factory

Conforme o projeto cresce, é bem comum cada classe de teste ter o seu próprio jeito de criar uma Account, um Contact, uma Opportunity. Isso funciona no começo, mas vira um problema real quando alguma regra do negócio muda — por exemplo, um campo que virou obrigatório para todos os clientes. Se você tem esse insert repetido em 80 classes diferentes, alguém vai ter que abrir e corrigir arquivo por arquivo, um por um.

A solução recomendada é centralizar essa criação de dados numa classe utilitária reutilizável, chamada de Test Data Factory (ou "fábrica de dados de teste"):

@IsTest
public class TestDataFactory {

    public static Account createAccount(String accountName, Boolean doInsert) {
        Account acc = new Account(Name = accountName);

        if (doInsert) {
            insert acc;
        }

        return acc;
    }
}

Com isso, qualquer classe de teste do projeto pode chamar TestDataFactory.createAccount('Conta Teste', true) em vez de repetir a mesma criação. Se amanhã um campo novo se tornar obrigatório, você ajusta uma única linha, num único lugar, e todas as classes que usam a fábrica já herdam a atualização automaticamente. É sobre padronização e facilidade de manutenção — vale a pena mesmo em projetos pequenos, porque eles tendem a crescer.

Fonte oficial: Salesforce Trailhead — Get Data for Your Tests

Beleza, com os dados sob controle, chegou a hora de encarar de frente aquele número que todo mundo comenta, mas nem sempre entende direito: os famosos 75% de cobertura.


6. Cobertura de código: o que é, e o que NÃO é

A fórmula (sem mistério)

Cobertura = linhas cobertas ÷ (linhas cobertas + linhas não cobertas) × 100

Exemplo: se sua classe tem 8 linhas executáveis, e os testes passaram por 6 delas, sua cobertura é 6 ÷ 8 × 100 = 75%.

Duas coisas importantes:

  • Comentários, linhas em branco e chaves soltas não contam — a conta é feita só em cima de linhas que realmente executam alguma instrução.
  • A própria classe de teste (@IsTest) fica de fora da conta. Ou seja, tecnicamente é errado dizer "minha classe de teste tem 80% de cobertura". O certo é dizer: "os testes cobriram 80% da classe que ela testa".

A regra dos 75% para ir pra produção

Para o Salesforce aceitar seu deploy em produção:

  • Pelo menos 75% do código Apex da organização precisa estar coberto.
  • Todos os testes executados precisam passar (não adianta ter 90% de cobertura se um teste está falhando).
  • Todo trigger precisa ter, no mínimo, alguma cobertura.

Isso está documentado oficialmente aqui: Salesforce — Testing Best Practices

Mas vale reforçar algo que vem lá da seção 0: os 75% precisam ser a consequência natural de testes bem-feitos — não a meta em si. Um projeto que persegue só o número, sem escrever Assert de verdade, é o famoso "painel verde" que não garante absolutamente nada por trás.

Atenção: nem todo deploy calcula a cobertura da mesma forma

Existem 3 "níveis de teste" (test levels) que se comportam de forma diferente:

Nível Como a cobertura é calculada
RunLocalTests Considera a cobertura geral da organização (soma tudo, tira a média).
RunAllTestsInOrg Igual ao anterior, mas também roda testes vindos de pacotes instalados.
RunSpecifiedTests Aqui muda o jogo: cada classe e trigger que está sendo enviado no deploy precisa bater 75% sozinho, e não como média geral.

Por que isso importa na prática

Imagine que você tem:

  • ClasseA: 90% de cobertura
  • ClasseB: 60% de cobertura
  • Média geral: 75%

Se o deploy usa RunLocalTests, a média alta de ClasseA "carrega nas costas" a ClasseB, e os 75% de média já bastam — passa tranquilo.

Mas se o deploy usa RunSpecifiedTests e a ClasseB está entre os itens sendo enviados, essa proteção da média desaparece: ela vai falhar sozinha, porque 60% é menor que 75%, mesmo a média geral estando ok. Esse é um dos erros que mais pega gente de surpresa na hora de um deploy — o número "geral" que você vê no painel da org pode simplesmente não valer nessa situação, e o deploy trava.

Entendido o número mágico e as pegadinhas dele? Então vamos resolver uma dúvida que praticamente todo mundo tem no início: "preciso mesmo criar uma classe de teste pra cada classe do sistema?"


7. "Preciso de uma classe de teste pra cada classe do sistema?"

Tecnicamente, não existe uma exigência técnica de que ShippingFeeService precise, obrigatoriamente, de uma ShippingFeeServiceTest. O que gera cobertura é a linha ser executada por algum teste, não o nome da classe que a chamou.

Analogia: a viagem de carro

  • A classe de teste é o carro.
  • As classes que ele passa no caminho são as ruas.
  • Toda rua que o carro percorre fica marcada como "visitada" no mapa.
  • O sistema não exige que o carro se chame igual à rua.

Então é perfeitamente possível que um teste de CheckoutControllerTest acabe passando por CheckoutController → CheckoutService → ShippingFeeService, e as três ganharem cobertura — mesmo sem existir uma ShippingFeeServiceTest dedicada.

Então quando vale a pena criar uma classe de teste dedicada?

Crie uma classe própria quando:

  • A classe tem regras de negócio relevantes e específicas.
  • Ela tem bastante lógica condicional (vários if/else, retornos diferentes, exceções).
  • Ela é usada por várias outras classes diferentes (então merece ser validada isoladamente).
  • Ela lida com processamento assíncrono ou callouts (que precisam de mocks — a gente chega lá na próxima seção).
  • Seria difícil, olhando o projeto, saber "qual teste garante que essa classe funciona".

A convenção NomeDaClasseTest (ex: ShippingFeeServiceTest para ShippingFeeService) não é uma exigência da plataforma, mas é uma prática forte de organização — facilita muito a manutenção do projeto, principalmente quando o time cresce.

O risco de depender só da cobertura "de carona"

Imagina uma esteira de produção principal que, no meio do caminho, despacha uma peça pra uma roldana menor só pra formatar alguma coisa, e depois recebe a peça de volta. Nesse contato rápido, a tal roldana "de apoio" entra nas estatísticas — ela ganha cobertura só porque a esteira principal passou por ela durante um teste maior.

O problema aparece quando o processo muda. Se amanhã alguém decide remover essa etapa de formatação, a esteira principal para de chamar a roldana. Só que ela continua lá, ativa, fazendo parte do sistema — e de uma hora pra outra, o mapa de cobertura dela despenca, porque na verdade nunca existiu um teste validando ela de verdade, só um teste esbarrando nela sem querer.

Componentes precisam de testes isolados e específicos que garantam a própria integridade deles, independente de quem os chama. Por isso a pergunta certa não é só:

"Algum teste passa por essa classe?"

É também:

"Existe algum teste que prove, especificamente, o comportamento dessa classe?"

Se você chegou até aqui, já domina tudo que precisa pro dia a dia. A partir daqui a conversa fica um pouco mais avançada — então fique à vontade pra ler agora ou guardar pra uma segunda leitura, depois que praticar o básico. Sem pressa nenhuma.


8. Indo além do básico: casos mais avançados

Depois que a base — Arrange/Act/Assert, isolamento de dados, cobertura — estiver sólida, existem alguns cenários do dia a dia que merecem atenção própria. É aqui que a gente separa quem só "sabe escrever um teste" de quem realmente entende o que está protegendo.

8.1 De System.assert para a classe Assert

Durante muitos anos, a única forma de validar um resultado em Apex era com os métodos do System: System.assert(), System.assertEquals() e System.assertNotEquals(). Eles continuam funcionando normalmente até hoje, sem nenhum plano de serem desativados.

Só que, a partir da atualização Winter '23, a Salesforce lançou uma classe dedicada, chamada Assert, com métodos mais expressivos: Assert.areEqual(), Assert.areNotEqual(), Assert.isTrue(), Assert.isFalse(), Assert.isNull(), Assert.isNotNull(), entre outros. A vantagem não é só estética — é sobre expressividade. Compare:

// Estilo antigo (ainda funciona, mas é mais genérico)
System.assertEquals(25, actualFee, 'mensagem de erro');

// Estilo atual (recomendado pela própria Salesforce)
Assert.areEqual(25, actualFee, 'mensagem de erro');

Quando um teste falha, o log de erro da classe Assert costuma ser mais claro sobre exatamente o que divergiu, o que reduz bastante o tempo gasto investigando o motivo da falha. Por isso, a recomendação atual é usar a classe Assert em qualquer código novo, e ir migrando aos poucos o código antigo que ainda usa System.assert.

Fonte oficial: Salesforce Developers Blog — Write Clear and Intentional Apex Assertions with the New Assert Class

8.2 Testando quando as coisas dão errado: exceções

Testar o "caminho feliz" é necessário, mas não é suficiente. Testar quando o código deveria falhar é igualmente importante — talvez até mais, porque é justamente ali que moram as regras de proteção do negócio.

Imagine uma regra que proíbe dar mais de 40% de desconto sem aprovação de um gerente. O teste dessa regra precisa forçar o erro de propósito, capturar essa exceção com um bloco try/catch, e confirmar que ela realmente aconteceu:

@IsTest
static void shouldBlockDiscountsAboveLimitWithoutApproval() {
    try {
        Test.startTest();
        DiscountService.applyDiscount(50, false);
        Test.stopTest();

        // Se o código chegou até aqui, a exceção esperada NÃO foi lançada.
        Assert.fail('Era esperado um erro para desconto acima do limite sem aprovação.');
    } catch (DiscountService.DiscountNotAllowedException e) {
        Assert.isTrue(
            e.getMessage().contains('40%'),
            'A mensagem de erro deveria mencionar o limite de 40%.'
        );
    }
}

Repara no Assert.fail() logo depois da chamada. Ele parece estranho à primeira vista — afinal, você já está forçando o erro, por que precisa de mais uma linha ali? A resposta é simples: essa linha é a sua proteção contra o "erro engolido em silêncio". Se, meses depois, alguém remover sem querer a validação de desconto lá na classe original, o erro deixa de acontecer, o fluxo passa direto pelo try sem cair no catch, e sem o Assert.fail() o teste passaria tranquilamente — aprovando um desconto de 50% que nunca deveria ter sido aceito. Com o Assert.fail() ali, o teste quebra imediatamente e avisa exatamente o que sumiu.

8.3 Testando integrações externas sem depender do mundo real: HttpCalloutMock

Quando o código faz uma chamada para uma API externa (um callout), o Salesforce não permite que esse callout de verdade aconteça durante um teste. Isso é intencional: seu teste não pode depender de um servidor de terceiros estar disponível, respondendo rápido, ou nem mesmo de a internet estar funcionando naquele momento.

A solução é implementar a interface HttpCalloutMock, que devolve uma resposta falsa e controlada no lugar da resposta real:

@IsTest
global class ShippingApiMock implements HttpCalloutMock {
    global HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setHeader('Content-Type', 'application/json');
        response.setBody('{"status":"ok"}');
        response.setStatusCode(200);
        return response;
    }
}

E no teste, você avisa o Apex para usar essa simulação em vez de tentar sair pra internet de verdade:

@IsTest
static void shouldHandleSuccessfulApiResponse() {
    Test.setMock(HttpCalloutMock.class, new ShippingApiMock());

    Test.startTest();
    // código que faz o callout aqui
    Test.stopTest();

    // Assert conferindo o comportamento esperado
}

Isso não é "trapacear" o teste, mesmo parecendo à primeira vista. A ideia é separar responsabilidades: seu teste não precisa (e nem deveria) validar se o servidor da API externa está de pé — isso é responsabilidade de quem cuida daquele servidor. O que está sob seu controle, e é isso que o teste precisa provar, é como o seu sistema reage ao que chega de fora — inclusive aos cenários ruins. Um bom teste de callout não só simula uma resposta de sucesso, mas também simula uma falha (por exemplo, um erro 500) para confirmar que o código trata isso com elegância, em vez de simplesmente quebrar.

Fonte oficial: Salesforce — Testing HTTP Callouts by Implementing the HttpCalloutMock Interface

8.4 Testando com permissões diferentes: System.runAs()

É bem comum escrever e rodar os testes com o usuário administrador — que enxerga praticamente tudo na organização. Mas o usuário final, na ponta, normalmente tem acesso bem mais restrito, e regras de compartilhamento de registros que fazem diferença real no comportamento do sistema.

Com o System.runAs(), é possível mudar o contexto do teste para um usuário específico, e validar como o sistema se comporta sob as regras de compartilhamento (sharing rules) daquele perfil:

@IsTest
static void shouldRespectSharingRulesForRestrictedUser() {
    User restrictedUser = TestDataFactory.createUser('Standard User');

    System.runAs(restrictedUser) {
        Test.startTest();
        // código executado como esse usuário restrito
        Test.stopTest();

        // Assert conferindo que as regras de compartilhamento foram respeitadas
    }
}

Vale um detalhe técnico importante: o runAs aplica as regras de compartilhamento de registros (sharing) daquele usuário, mas, por padrão, não simula permissões de objeto ou de campo. Ainda assim, é a ferramenta certa para provar que sua lógica se comporta corretamente diante de um usuário com visibilidade restrita — e não só diante do administrador, que enxerga tudo.

Fonte oficial: Salesforce — Using the runAs Method

8.5 Cuidado: o anti-padrão do Test.isRunningTest()

O método Test.isRunningTest() permite que o código descubra, em tempo de execução, se está sendo chamado a partir de um teste. Ele tem usos legítimos e pontuais — por exemplo, para decidir usar um mock em vez de fazer um callout de verdade.

O problema é quando ele é usado para mudar o comportamento de negócio dependendo se é teste ou não:

// Isso é um alerta vermelho — evite
if (Test.isRunningTest()) {
    resultado = calculoSimplificado();
} else {
    resultado = calculoCompletoEDenso();
}

Quando isso acontece, você deixa de testar o sistema real e passa a validar, com sucesso garantido, um comportamento que nunca vai rodar em produção. Isso mascara gargalos de performance, esconde bugs reais, e cria uma falsa sensação de segurança — o painel fica verde, mas ele está mentindo sobre o que foi validado. Se uma parte do seu código só existe "pro caminho de teste", vale se perguntar: o que, exatamente, está sendo protegido por esse teste?

8.6 Uma nota conceitual: testes unitários x testes de integração

Você provavelmente vai esbarrar nesses dois termos:

  • Teste unitário: testa uma única unidade de código isoladamente (um método, uma classe), normalmente usando mocks para "cortar" qualquer dependência externa.
  • Teste de integração: valida o fluxo de ponta a ponta, tocando de fato no banco de dados e em várias camadas do sistema juntas.

Vale saber que, na prática, o desenvolvimento em Apex tende a ser guiado principalmente por testes com características de integração — afinal, boa parte da lógica de negócio no Salesforce está amarrada ao banco de dados (via SOQL/DML) e às regras da plataforma, o que torna difícil isolar tudo por completo como num teste unitário "puro". Isso não é um problema — é só uma característica do ecossistema, e ajuda a explicar por que a seção 5 (isolamento de dados) é tão central neste guia.

Respirou? Ótimo. Agora vamos falar dos tropeços mais comuns de quem está começando — vale a pena dar uma olhada mesmo que você já ache que não vai cair em nenhum deles, porque quase todo mundo já caiu em pelo menos um.


9. Erros comuns de quem está começando

Erro Por que é um problema O que fazer no lugar
Escrever o teste só para "bater 75%" Gera cobertura sem provar nada (é o "teste fraco" da seção 2) Sempre incluir Assert comparando o resultado esperado
Usar Id de um registro fixo que existe na sua org Funciona na sua sandbox e quebra em qualquer outro ambiente Sempre criar o próprio dado dentro do teste
Esquecer o Test.startTest() / Test.stopTest() Código assíncrono pode não ser processado de forma confiável no teste Sempre envolver a parte testada nesse par
Testar só o "caminho feliz" Bugs geralmente aparecem nos casos de borda, não no caso óbvio Testar também nulo, negativo, vazio, limite exato da regra
Usar SeeAllData=true sem necessidade Cria dependência de dados que você não controla Criar os dados no próprio teste (ou via @TestSetup)
Forçar uma exceção sem Assert.fail() depois Se a exceção parar de acontecer no futuro, o teste passa sem avisar nada Sempre incluir Assert.fail() logo após o trecho que deveria lançar erro
Usar Test.isRunningTest() para mudar lógica de negócio Você passa a validar um comportamento que nunca roda em produção Reserve esse método só para trocas técnicas pontuais (como usar um mock)
Assumir que o Id de um registro é sequencial Salesforce não garante essa ordem Nunca basear lógica de teste na ordem numérica dos Ids
Testar só com 1 registro quando o código roda em lote (trigger) Não simula o "modo real" de uso, onde vários registros são salvos juntos Testar com pelo menos ~20 registros de uma vez

Se você se reconheceu em alguma dessas linhas, sem crise — é literalmente assim que todo mundo aprende. Bora agora colocar isso tudo em prática, passo a passo, do zero.


10. Passo a passo: escrevendo sua primeira classe de teste do zero

  1. Olhe para a classe que você quer testar e liste as regras de negócio dela. No nosso exemplo: "nulo/negativo retorna null", "premium é grátis", "≥200 é grátis", "resto paga 25".
  2. Para cada regra, pense: qual é o cenário que a comprova? Cada cenário vira um método de teste.
  3. Nomeie o método descrevendo o comportamento, não a mecânica (shouldGiveFreeShippingToPremiumCustomer, e não test1).
  4. Escreva o Arrange: defina os dados de entrada e o valor que você espera como saída.
  5. Escreva o Act: entre Test.startTest() e Test.stopTest(), chame o método que você quer validar.
  6. Escreva o Assert: compare o resultado real com o esperado, com uma mensagem clara.
  7. Rode o teste (pela IDE ou linha de comando — veja a próxima seção) e confira se ele passa.
  8. Quebre o teste de propósito (mude temporariamente algo no código) e veja se o teste realmente falha. Se ele continuar passando mesmo com o código errado, ele não está testando nada de verdade — volte ao passo 6.
  9. Repita para os demais cenários, incluindo os inválidos e os de fronteira.

O passo 8 é o mais ignorado por quem está começando — e é, sinceramente, o mais valioso de todos. Um teste que nunca falha, mesmo quando você quebra o código de propósito, não é um teste. É só uma ilusão de segurança, um "laudo assinado em branco". A pergunta que fica é sempre essa: esse teste realmente prova que o sistema se comporta como deveria, ou ele só passou por ali e foi embora de mãos abanando?

Com o passo a passo na manga, só falta uma coisa: saber onde clicar (ou o que digitar) pra realmente ver esse teste rodando.


11. Como rodar e conferir a cobertura

Vendo a cobertura direto na classe, no Developer Console

Lembra lá da seção 2, da analogia do inspetor caminhando pelos cômodos do prédio? Pois é — o Developer Console te deixa literalmente enxergar esse mapa térmico, linha por linha, dentro da própria classe. Isso é extremamente útil na prática, porque em vez de só saber "minha cobertura está em 83%", você consegue apontar o dedo exatamente pra linha que ainda não foi visitada por nenhum teste.

O primeiro passo é rodar a sua classe de teste normalmente. Abra a classe de teste no Developer Console, clique em Run Test, e depois de terminar, dá uma olhada no painel inferior:

Developer Console mostrando o painel de testes com 5 de 5 testes aprovados, e o painel de cobertura geral mostrando ShippingFeeService com 100% e 6/6 linhas cobertas
Painel de testes do Developer Console com os cinco testes aprovados e cobertura geral de 100%.

Repara em dois pontos ali: na aba Tests, embaixo, todos os 5 métodos da ShippingFeeServiceTest passaram (aquele ✓ verde). E no painel Overall Code Coverage, do lado direito, o Developer Console já te mostra, por classe, o percentual e a proporção exata de linhas cobertas — nesse caso, ShippingFeeService está em 100%, com 6 de 6 linhas percorridas pelos testes.

Só que o mais interessante mesmo é abrir a classe de produção (não a de teste) logo depois de rodar os testes. O próprio editor de código pinta cada linha de acordo com o resultado:

Classe ShippingFeeService com 100% de cobertura, todas as linhas executáveis destacadas em azul
Com 100% de cobertura, todas as linhas executáveis aparecem destacadas em azul.

Aqui, no canto superior esquerdo, o dropdown Code Coverage: All Tests 100% confirma o número, e toda linha executável do método calculate aparece destacada em azul — ou seja, todas foram visitadas por algum teste. É o equivalente visual de "o inspetor esteve em todos os cômodos".

Agora olha o que acontece quando a cobertura não está completa:

Mesma classe ShippingFeeService, agora com 83% de cobertura, e a linha "return 0;" destacada em vermelho
Com 83% de cobertura, a linha não executada pelo teste aparece em vermelho.

Nessa versão, o dropdown mostra Code Coverage: All Tests 83%, e quase tudo continua azul — exceto a linha return 0;, que aparece destacada em vermelho. Isso quer dizer, de forma bem direta: nenhum teste, em nenhum momento, fez essa linha específica ser executada. Repara que o if logo acima dela (if (premiumCustomer == true || orderTotal >= 200)) está em azul — a condição chegou a ser avaliada — mas ela nunca avaliou como verdadeira durante os testes, então o return 0; de dentro do bloco nunca rodou.

Nesse exemplo específico, isso aconteceria se a classe de teste tivesse só os métodos shouldChargeStandardFee, shouldReturnNullForInvalidTotal e shouldReturnNullForNegativeTotal — nenhum deles testa um cliente premium nem um pedido de R$ 200 ou mais, então a linha do frete grátis nunca é alcançada. E o mais legal é que você não precisa adivinhar isso: a cor vermelha já aponta exatamente qual comportamento está faltando validar. Nesse caso, a solução seria voltar a incluir os métodos shouldGiveFreeShippingAtBoundary e shouldGiveFreeShippingToPremiumCustomer — que são justamente os que forçam essa condição a ser verdadeira.

Essa é, na prática, a ferramenta mais rápida que você tem pra responder "o que ainda falta eu testar nessa classe?" — sem precisar ficar lendo linha por linha tentando adivinhar de cabeça.

No VS Code

  1. Ative a configuração retrieve-test-code-coverage.
  2. Abra a classe de teste.
  3. Rode Run Test (um método) ou Run All Tests (a classe toda).
  4. Veja o resultado no painel Output, incluindo quais linhas ficaram sem cobertura.

Pela Salesforce CLI

Rodar uma classe específica:

sf apex run test --class-names ShippingFeeServiceTest --code-coverage --detailed-coverage --result-format human --wait 20 --target-org MinhaSandbox

Rodar todos os testes locais da org:

sf apex run test --test-level RunLocalTests --code-coverage --result-format human --wait 30 --target-org MinhaSandbox

Validando um deploy antes de fazer de verdade

sf project deploy validate --manifest manifest/package.xml --test-level RunLocalTests --target-org Producao --wait 60

Essa validação é a mais próxima possível do que vai acontecer no deploy real — vale rodar antes de qualquer envio para produção.

E antes de bater o martelo e considerar sua classe pronta, vamos fazer juntos(as) uma última conferida.


12. Checklist final

Antes de considerar sua classe de teste pronta, confira:


Resumindo tudo em uma frase

Cobertura mostra por onde o código passou. O Assert prova que o que aconteceu ali estava correto. Uma boa classe de teste sempre entrega as duas coisas — e é escrita pensando no comportamento que ela protege, não só no número de 75% que ela precisa bater.

Se você chegou até aqui, já sabe mais sobre teste em Apex do que uma boa parte de quem já trabalha com Salesforce há um tempo — sério. E fica um convite pra levar daqui pra frente: da próxima vez que terminar de escrever um teste, para antes de marcar como concluído e se pergunte — esse teste realmente comprova, sem sombra de dúvida, que o sistema se comporta como deveria? Ou ele só passou pela sala, deu uma olhada rápida, e foi embora de mãos abanando?

Bons testes, e qualquer dúvida, você sabe onde esse guia mora.

Fontes usadas neste guia

Quer continuar aprendendo Salesforce e OmniStudio na prática?

Conheça meus conteúdos, o projeto OmniStudio 365 e o curso OmniStudio na Prática 2026.

Conhecer o OmniStudio 365

Sobre a autora

Sou Monique da Silva, desenvolvedora Salesforce com atuação em OmniStudio/Vlocity. Atualmente estou desenvolvendo skills em Agentforce, Apex, LWC e Revenue Cloud.

Uso este espaço para compartilhar conteúdo prático sobre Salesforce e conectar minha experiência profissional com o que estou estudando sobre arquitetura de software e soluções com IA.

Não sou dona da verdade. Se você não concorda ou tem algo para contribuir, comente e vamos aprender juntos.