Início › Desafios › Desafio 03 › Gabarito
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.000e2.000.000passam 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 != nullcobre a linha e aceita qualquer um dos três resultados. Não prova regra nenhuma. - Depender de dado da org. Usar
SeeAllData=trueou 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
nullsó para cobrir a linha doifconta 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 sharinge grava um campo. Criar um usuário de perfil restrito e rodar a chamada dentro de umSystem.runAsprova 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.