InícioDesafiosDesafio 03 › Gabarito

Gabarito · Desafio 03 Nível: Intermediário

Gabarito: o teste que realmente testa

Esta página destrincha a solução do terceiro desafio. Se você ainda não tentou escrever a classe de teste, vale voltar e passar meia hora nela: o valor do exercício está em sentir a diferença entre um teste que passa e um teste que prova algo. Se já tentou, use esta página para conferir se o seu teste ficaria vermelho ao sabotar o método.

Spoiler à frente. A solução completa está logo abaixo. Voltar para o desafio

O problema em uma frase

Cobertura de código mede quantas linhas rodaram durante os testes; ela não mede se o resultado estava certo. Um método pode chegar a 100% de cobertura com um teste que nunca verifica nada, e o Salesforce deixa você fazer deploy assim numa boa. O erro clássico é este: o time exige 75% de cobertura, alguém escreve testes só para bater a meta, e a suíte inteira vira um carimbo que aprova qualquer mudança. No dia em que alguém troca >= por > numa regra de faixa, nenhum teste reclama, e o bug sobe assinado como testado.

A solução gira em torno de uma ideia: um bom teste é aquele que fica vermelho quando o código erra. Todo o resto (dado próprio, fronteira, bulk, caminho negativo) existe para servir a isso.

A solução completa

@isTest
private class ClassificadorReceitaTest {

    // Cria o cenario uma vez para todos os metodos de teste.
    // Nenhuma conta vem da org: o teste e dono do proprio dado.
    @TestSetup
    static void criarDados() {
        List<Account> contas = new List<Account>{
            new Account(Name = 'Fronteira Hot',  AnnualRevenue = 1000000),
            new Account(Name = 'Acima de Hot',   AnnualRevenue = 5000000),
            new Account(Name = 'Fronteira Warm', AnnualRevenue = 100000),
            new Account(Name = 'Meio de Warm',   AnnualRevenue = 500000),
            new Account(Name = 'Abaixo de Warm', AnnualRevenue = 99999),
            new Account(Name = 'Receita nula',   AnnualRevenue = null)
        };
        insert contas;
    }

    @isTest
    static void classificaCadaFaixaNaFronteira() {
        List<Account> contas = [SELECT Id, AnnualRevenue FROM Account];

        Test.startTest();
        ClassificadorReceita.classificar(contas);
        Test.stopTest();

        Map<String, String> ratingPorNome = new Map<String, String>();
        for (Account c : [SELECT Name, Rating FROM Account]) {
            ratingPorNome.put(c.Name, c.Rating);
        }

        System.assertEquals('Hot',  ratingPorNome.get('Fronteira Hot'),  'Exatamente 1 milhao e Hot');
        System.assertEquals('Hot',  ratingPorNome.get('Acima de Hot'),   'Acima de 1 milhao e Hot');
        System.assertEquals('Warm', ratingPorNome.get('Fronteira Warm'), 'Exatamente 100 mil e Warm');
        System.assertEquals('Warm', ratingPorNome.get('Meio de Warm'),   'Entre 100 mil e 1 milhao e Warm');
        System.assertEquals('Cold', ratingPorNome.get('Abaixo de Warm'), 'Abaixo de 100 mil e Cold');
        System.assertEquals('Cold', ratingPorNome.get('Receita nula'),   'Receita nula e Cold');
    }

    @isTest
    static void classificaDuzentasContas() {
        List<Account> lote = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            lote.add(new Account(Name = 'Bulk ' + i, AnnualRevenue = 250000));
        }
        insert lote;

        Test.startTest();
        ClassificadorReceita.classificar(lote);
        Test.stopTest();

        Integer warm = [SELECT COUNT() FROM Account WHERE Name LIKE 'Bulk %' AND Rating = 'Warm'];
        System.assertEquals(200, warm, 'As 200 contas do lote precisam sair como Warm');
    }

    @isTest
    static void naoQuebraComEntradaVazia() {
        Test.startTest();
        Integer dmlAntes = Limits.getDmlStatements();
        ClassificadorReceita.classificar(null);
        ClassificadorReceita.classificar(new List<Account>());
        Integer dmlDepois = Limits.getDmlStatements();
        Test.stopTest();

        System.assertEquals(dmlAntes, dmlDepois, 'Entrada nula ou vazia nao pode disparar DML');
    }
}

Destrinchando parte por parte

1. O @TestSetup e o dado que o teste possui

@TestSetup
static void criarDados() {
    List<Account> contas = new List<Account>{
        new Account(Name = 'Fronteira Hot',  AnnualRevenue = 1000000),
        new Account(Name = 'Fronteira Warm', AnnualRevenue = 100000),
        new Account(Name = 'Abaixo de Warm', AnnualRevenue = 99999),
        new Account(Name = 'Receita nula',   AnnualRevenue = null)
    };
    insert contas;
}

O método marcado com @TestSetup roda uma vez antes de cada método de teste, e o Salesforce dá a cada teste uma cópia limpa desses registros. Isso deixa o cenário em um lugar só e evita repetir insert em toda parte.

O ponto que realmente importa aqui é o que não está escrito: não há @isTest(SeeAllData=true). Sem essa anotação, o teste enxerga apenas o que ele mesmo criou, e é assim que tem que ser. Um teste que depende de "a conta Acme que existe na minha org" passa na sua máquina e falha na de quem clonar o repositório, ou pior, muda de resultado quando alguém edita aquela conta na produção. Teste bom é hermético: ele carrega o próprio mundo.

Note também que cada conta tem um nome que descreve seu papel no teste. Não é enfeite: quando um assert falha, a mensagem cita o nome, e você sabe na hora qual faixa quebrou.

2. O teste de fronteira (o coração do exercício)

System.assertEquals('Hot',  ratingPorNome.get('Fronteira Hot'),  'Exatamente 1 milhao e Hot');
System.assertEquals('Warm', ratingPorNome.get('Fronteira Warm'), 'Exatamente 100 mil e Warm');
System.assertEquals('Cold', ratingPorNome.get('Abaixo de Warm'), 'Abaixo de 100 mil e Cold');

Aqui está o que separa este teste de um teste decorativo. A regra tem duas fronteiras: 1.000.000 e 100.000. O bug mais comum em regra de faixa é errar o lado da comparação: usar > onde deveria ser >=, ou vice-versa. Esse erro só aparece exatamente no valor da fronteira. Uma conta com 150.000 vira Warm tanto com >= quanto com > em cima de 100.000; ela nunca acusa o defeito.

Por isso o cenário coloca contas exatamente em 1.000.000 e 100.000, e uma logo abaixo em 99.999. Testar os dois lados de cada fronteira é o que prende a regra. Se você só testou o meio das faixas, seu teste passa mesmo com o código trocado, e a cobertura vai continuar dizendo que está tudo bem.

Repare ainda que o assert compara o valor ('Warm'), não a existência. Um assert do tipo "o Rating não é nulo" cobre a linha e não prova a regra: qualquer um dos três valores o satisfaz.

3. O bloco de 200 contas

List<Account> lote = new List<Account>();
for (Integer i = 0; i < 200; i++) {
    lote.add(new Account(Name = 'Bulk ' + i, AnnualRevenue = 250000));
}
insert lote;

Test.startTest();
ClassificadorReceita.classificar(lote);
Test.stopTest();

Integer warm = [SELECT COUNT() FROM Account WHERE Name LIKE 'Bulk %' AND Rating = 'Warm'];
System.assertEquals(200, warm, 'As 200 contas do lote precisam sair como Warm');

Testar com um registro só esconde a classe inteira de bugs que aparece em massa: consulta ou DML dentro de laço, coleção que não acumula, limite de governança. O padrão do mercado é exercitar todo método com 200 registros, o teto de um lote de trigger.

O Test.startTest() e o Test.stopTest() em volta da chamada não são decoração: eles dão à parte medida um conjunto de limites de governança zerado. Assim, o consumo do preparo dos dados não polui a medição, e você prova que a chamada de verdade cabe sozinha nos limites de uma transação. O raciocínio completo sobre esses limites está no artigo sobre governor limits em Apex.

4. O caminho negativo com assert de verdade

Test.startTest();
Integer dmlAntes = Limits.getDmlStatements();
ClassificadorReceita.classificar(null);
ClassificadorReceita.classificar(new List<Account>());
Integer dmlDepois = Limits.getDmlStatements();
Test.stopTest();

System.assertEquals(dmlAntes, dmlDepois, 'Entrada nula ou vazia nao pode disparar DML');

A regra pedia que lista nula e lista vazia não estourassem. É tentador escrever só a chamada e confiar que "se não deu erro, passou". Mas isso é frágil: um teste sem assert nenhum é lido pelo Salesforce como sucesso mesmo que a intenção fosse outra. Medir Limits.getDmlStatements() antes e depois transforma a intenção em verificação: além de não lançar exceção, o método provou que respeitou a saída antecipada e não gastou uma operação de banco à toa. Se alguém remover o if de guarda no método, este assert acusa.

Erros comuns

  • Testar só o meio das faixas. Contas em 500.000 e 2.000.000 passam com a regra certa e com a regra trocada. Sem os valores de fronteira, o teste é decorativo.
  • Assert de existência em vez de valor. Checar Rating != null cobre a linha e aceita qualquer um dos três resultados. Não prova regra nenhuma.
  • Depender de dado da org. Usar SeeAllData=true ou consultar contas que "já estão lá" faz o teste passar na sua org e quebrar na de outra pessoa.
  • Caminho negativo sem assert. Chamar com null só para cobrir a linha do if conta cobertura, mas não verifica que o comportamento é o esperado.
  • Testar com um registro só. Passa no caso feliz e não pega o bug que aparece em lote. O padrão é 200.
  • Confundir cobertura com qualidade. 90% de cobertura com asserts fracos é pior que 75% com asserts que prendem a regra: dá a sensação de segurança sem a segurança.

Como saber se a sua solução passou

O teste ficar verde é o mínimo. O critério objetivo deste desafio é o experimento de sabotagem, que na indústria se chama mutation testing: você introduz um bug de propósito e confere se o teste o pega.

// Em ClassificadorReceita, troque
//     } else if (receita != null && receita >= 100000) {
// por
//     } else if (receita != null && receita > 100000) {
// A conta 'Fronteira Warm' (100.000) passa a cair em 'Cold'.

Com o teste acima, essa mutação faz o assert 'Exatamente 100 mil e Warm' ficar vermelho na hora. Esse é o sinal de que o teste testa. Se você fizer a mesma sabotagem e a suíte continuar verde, o teste está cobrindo linha sem verificar comportamento, e é hora de reforçar os asserts. Desfaça a mutação depois de confirmar.

Um passo além

Para levar a suíte ao nível que se espera em produção, três evoluções:

  • Testar a permissão com System.runAs. A classe é with sharing e grava um campo. Criar um usuário de perfil restrito e rodar a chamada dentro de um System.runAs prova que o método se comporta sob as permissões de quem o aciona, não sob acesso total. O raciocínio de segurança por trás disso está em segurança em Apex: CRUD, FLS e sharing.
  • Assertar o consumo de governança no bulk. Depois do Test.stopTest(), checar que o número de DML e de SOQL da chamada não cresce com a quantidade de contas transforma o teste de 200 registros numa prova de bulkificação, não só de resultado. É o mesmo reflexo do primeiro desafio, agora dentro do teste.
  • Adotar a sabotagem como hábito. Toda vez que escrever um teste novo, quebre a regra de propósito uma vez e confirme o vermelho antes de dar por pronto. É trinta segundos que separam o teste que protege do teste que só decora a cobertura.

O que fica deste desafio: cobertura é quanto do código rodou; teste é quanto do comportamento você prendeu. Em entrevista técnica de Salesforce, "qual a diferença entre cobertura e um bom teste" é uma das perguntas que mais separa quem só bateu a meta de quem entende para que a suíte serve.