Cobertura alta pode esconder um teste que apenas executa linhas. O valor começa quando o cenário separa preparação, ação e verificação, mede o efeito observável e falha por uma razão clara quando o contrato do código deixa de ser cumprido.
O teste consulta a tarefa criada pelo Queueable e não encontra nada, ou a preparação dos dados gasta as consultas de que o serviço precisava. Test.startTest() e Test.stopTest() resolvem os dois casos: separam a ação medida da preparação, dão a ela um conjunto renovado de limites e fazem o trabalho assíncrono enfileirado executar antes das asserções.
Cobrir uma linha não prova o comportamento
O par não transforma cobertura em qualidade. O teste só prova algo quando compara um resultado esperado com o efeito real, e é aí que muita suíte verde falha.
O Salesforce exige cobertura para implantar Apex, mas a porcentagem responde apenas quais linhas foram executadas. Ela não responde se o desconto foi calculado corretamente, se um usuário sem acesso recebeu o bloqueio esperado ou se uma fila assíncrona atualizou o registro certo. Um teste pode alcançar o caminho feliz inteiro e ainda não possuir uma única asserção relevante.
Considere um serviço demonstrativo que fecha uma oportunidade e enfileira a criação de uma tarefa de acompanhamento. Um teste fraco cria a oportunidade, chama o serviço e termina. Se alguém alterar o status gravado de Closed Won para Prospecting, remover o vínculo da tarefa ou inverter uma condição, muitas das mesmas linhas continuam cobertas. A barra verde permanece, mas o contrato foi quebrado.
As boas práticas de teste do Apex Developer Guide pedem asserções sobre o resultado e dados criados pelo próprio teste. Sem asserção, o método só prova que nenhuma exceção escapou. Sem dados próprios, ele depende do que existe na org e pode passar numa sandbox e falhar na outra.
Uma forma simples de manter essa intenção visível é organizar cada método em três fases:
- Arrange: crie somente os dados necessários e confira pré-condições importantes.
- Act: execute a unidade de comportamento dentro de
Test.startTest()eTest.stopTest(). - Assert: consulte o estado final e compare valores concretos com o contrato esperado.
Esse formato combina bem com uma TestDataFactory para dados de teste, porque a factory reduz o ruído do Arrange sem esconder a ação que está sendo avaliada.
O que startTest e stopTest realmente isolam
O código executado durante um teste compartilha os limites de governança da transação. A preparação pode consumir consultas, DML e CPU antes de o serviço principal começar. Test.startTest() abre um contexto renovado de limites para a fase Act. Test.stopTest() encerra essa fase e devolve a execução ao contexto anterior do método de teste.
Cada método de teste pode chamar Test.startTest() e Test.stopTest() apenas uma vez. Segundo o guia, startTest adiciona um contexto ao teste em vez de reiniciar o contexto existente, e o código após stopTest volta aos limites que valiam antes do bloco.
Isso não significa limites ilimitados nem dois orçamentos para a aplicação usar em produção. O bloco existe para que o custo da montagem do cenário não esconda o comportamento da unidade avaliada. Se o serviço faz 101 consultas sozinho, ele continua falhando. Se a preparação gastou consultas antes do bloco, o serviço começa com o orçamento apropriado ao seu próprio contexto.
@IsTest
private class OpportunityClosingServiceTest {
@IsTest
static void shouldCloseOpportunityAndCreateFollowUp() {
Account account = new Account(Name = 'Conta demonstrativa');
insert account;
Opportunity opportunity = new Opportunity(
Name = 'Renovacao demonstrativa',
AccountId = account.Id,
StageName = 'Negotiation/Review',
CloseDate = Date.today().addDays(7)
);
insert opportunity;
Test.startTest();
OpportunityClosingService.close(opportunity.Id);
Test.stopTest();
Opportunity saved = [
SELECT StageName
FROM Opportunity
WHERE Id = :opportunity.Id
];
Task followUp = [
SELECT WhatId, Subject, Status
FROM Task
WHERE WhatId = :opportunity.Id
LIMIT 1
];
Assert.areEqual('Closed Won', saved.StageName,
'O servico deve fechar a oportunidade');
Assert.areEqual(opportunity.Id, followUp.WhatId,
'A tarefa deve pertencer a oportunidade fechada');
Assert.areEqual('Not Started', followUp.Status,
'A tarefa deve nascer pendente');
}
}
As três asserções olham para o que um consumidor do serviço enxerga: o estágio gravado, o vínculo da tarefa e o status inicial dela. Se alguém refatorar o serviço por dentro e mantiver esses efeitos, o teste segue verde. Se trocar Closed Won por outro valor, a falha diz exatamente qual efeito sumiu.
O Apex Developer Guide descreve o par como a forma de obter um conjunto novo de limites para o código testado. Essa separação é útil, mas medir cada contador por hábito pode amarrar o teste à implementação. Verifique Limits.getQueries() quando a quantidade de consultas faz parte do requisito, como em um algoritmo que promete uma consulta para qualquer lote. Para uma regra de negócio comum, prefira afirmar o resultado.
Um handler bulkificado é o caso típico em que o consumo faz parte do contrato: ele precisa processar uma coleção sem fazer query por registro. Aqui, capturar o contador antes e depois da ação mostra uma propriedade relevante:
Test.startTest();
Integer queriesBefore = Limits.getQueries();
OpportunityService.recalculate(opportunityIds);
Integer queriesUsed = Limits.getQueries() - queriesBefore;
Test.stopTest();
Assert.areEqual(1, queriesUsed,
'O lote deve usar uma unica consulta');
Esse assert deve acompanhar uma verificação do resultado, porque uma implementação que não consulta nada e também não atualiza nada consumiria zero consultas. O teste precisa provar eficiência e correção. O artigo sobre governor limits no Apex aprofunda por que volume muda o comportamento de código que parece correto com três registros, e o post sobre bulkificação com 200 registros mostra o padrão que esse assert protege.
Não use startTest para esconder uma preparação exagerada. Se o Arrange precisa de dezenas de consultas, reveja a factory, use @TestSetup quando os métodos compartilham a mesma base e insira coleções em uma única operação. Um contexto renovado ajuda a medir a ação, mas não corrige um teste caro e difícil de entender.
stopTest é o ponto de sincronização do assíncrono
Queueable, Batch, Scheduled Apex e métodos future não terminam no instante em que são enfileirados em produção. Em teste, você precisa de um ponto determinístico para observar o efeito. O trabalho assíncrono enfileirado depois de Test.startTest() é executado quando Test.stopTest() é chamado. Só então vêm as asserções. Se ainda está escolhendo qual modelo usar, o comparativo de Queueable, Batch e Schedulable ajuda a decidir antes de escrever o teste.
@IsTest
private class RenewalReminderJobTest {
@IsTest
static void shouldCreateReminderAfterQueueableRuns() {
Opportunity opportunity = new Opportunity(
Name = 'Renovacao de laboratorio',
StageName = 'Closed Won',
CloseDate = Date.today()
);
insert opportunity;
Test.startTest();
System.enqueueJob(new RenewalReminderJob(opportunity.Id));
Test.stopTest();
List<Task> reminders = [
SELECT WhatId, Subject, Status
FROM Task
WHERE WhatId = :opportunity.Id
];
Assert.areEqual(1, reminders.size(),
'Uma unica tarefa deve ser criada');
Assert.areEqual('Not Started', reminders[0].Status,
'O lembrete deve nascer pendente');
}
}
O erro comum é consultar a tarefa antes de Test.stopTest(). Nesse momento o Queueable foi aceito, mas o efeito ainda não foi materializado. Outro erro é afirmar somente que o enfileiramento aconteceu. Isso não prova que o trabalho terminou nem que produziu o resultado correto. O exemplo oficial da interface Queueable segue o mesmo caminho: enfileira dentro do bloco e, depois de stopTest, consulta o registro criado pelo job.
Batch Apex tem uma restrição própria: em teste, só uma execução do método execute acontece. Crie no máximo a quantidade de registros de um lote (200 por padrão) e verifique o efeito de start, execute e finish. Com mais registros do que cabem em um lote, o teste passa a depender de um comportamento que a plataforma não garante dentro do stopTest.
Evite transformar um teste em uma corrente de vários níveis assíncronos. A documentação de Queueable é explícita: encadear jobs dentro de um teste Apex gera erro, e a saída indicada é checar Test.isRunningTest() antes de enfileirar o filho. Por isso, teste cada etapa diretamente, chamando o job filho em outro método. A previsibilidade vale mais do que imitar uma operação completa dentro de um único método.
System.runAs responde outra pergunta
System.runAs(usuario) e Test.startTest() não são substitutos. O primeiro muda o contexto do usuário para o trecho executado. O segundo separa limites e controla a execução do assíncrono. Eles podem aparecer juntos quando o contrato exige comportamento diferente por usuário.
System.runAs(representativeUser) {
Test.startTest();
List<Opportunity> visibleRecords =
OpportunitySelector.visibleRenewals();
Test.stopTest();
Assert.areEqual(1, visibleRecords.size(),
'O representante deve receber apenas o registro compartilhado');
}
O runAs só pode ser usado em métodos de teste. Segundo o guia sobre runAs, ele aplica o compartilhamento de registros do usuário, mas não aplica permissões de objeto nem de campo. Para o exemplo acima provar visibilidade, o selector precisa ser declarado with sharing. Ele não garante sozinho que CRUD e FLS foram aplicados. O código ainda precisa escolher mecanismos como user mode, checagens explícitas ou Security.stripInaccessible, conforme o caso. O checklist de segurança Apex ajuda a separar acesso ao registro, ao objeto e ao campo.
Crie o usuário dentro do teste com o perfil ou permission set necessário ao cenário demonstrativo. Depois faça uma asserção que prove a diferença de visibilidade ou de resultado. Apenas envolver o serviço em runAs sem comparar o efeito deixa a intenção novamente implícita.
Casos negativos precisam de um contrato explícito
O caminho feliz raramente é o único comportamento relevante. Um serviço pode rejeitar ID nulo, impedir transição de estágio inválida ou devolver lista vazia quando não encontra registros. Cada condição merece um método de teste focado, com nome que descreva o resultado.
@IsTest
static void shouldRejectNullOpportunityId() {
Test.startTest();
try {
OpportunityClosingService.close(null);
Assert.fail('Era esperada uma excecao para ID nulo');
} catch (OpportunityClosingService.InvalidInputException error) {
Assert.areEqual('OpportunityId obrigatorio', error.getMessage(),
'A mensagem deve explicar a entrada invalida');
}
Test.stopTest();
}
Assert.fail() torna evidente que chegar àquela linha é uma falha. A Salesforce recomenda os métodos da classe Assert por deixarem a intenção e as mensagens mais claras em asserções Apex intencionais. Na mensagem, escreva a regra (A mensagem deve explicar a entrada invalida); os valores esperado e atual o próprio Assert já imprime.
Teste também a ausência de efeito quando ela importa. Se entrada vazia deve ser ignorada, compare contadores de registros antes e depois. Se uma falha parcial deve preservar itens válidos, verifique sucesso e erro separadamente. Quem mexer no serviço daqui a um ano vai descobrir pelo teste vermelho que aquela decisão era intencional.
Antes de aceitar um método de teste numa revisão, passe por estas perguntas:
- O nome do método descreve condição e resultado esperado?
- O Arrange cria dados próprios e somente o necessário?
- A ação principal está entre
Test.startTest()eTest.stopTest()? - O assíncrono termina antes da consulta que verifica o efeito?
- Cada assert prova comportamento observável e possui mensagem útil?
- Existe cenário negativo, de permissão, de vazio ou de volume quando o contrato exige?
- O teste falharia se a regra principal fosse removida?
A última pergunta pega o teste decorativo. Apague mentalmente a atualização, retorne o valor errado ou remova o filtro. Se o teste ainda passaria, a cobertura não protege a regra. Ajuste a asserção até a falha representar exatamente o risco que você quer impedir.
Perguntas frequentes
O que Test.startTest faz no Apex?
Ele inicia a fase de ação com um conjunto renovado de limites de governança. Assim, consultas e DML usados para preparar dados não distorcem a observação do serviço testado. O método não remove limites nem torna a execução equivalente a várias transações de produção.
O que Test.stopTest faz com Queueable e Batch Apex?
Ele faz o trabalho assíncrono enfileirado depois de Test.startTest() executar antes de devolver o controle ao método de teste. Depois de stopTest, consulte os registros ou outro efeito observável e faça as asserções. Em Batch, só uma execução de execute roda no teste.
Test.startTest aumenta os limites disponíveis no Apex?
Não. Ele separa um contexto renovado para a ação e depois restaura o contexto anterior. Código que excede o limite dentro dessa fase continua falhando com System.LimitException. O bloco serve para medir a ação longe do custo da preparação.
System.runAs testa CRUD e FLS automaticamente?
Não. System.runAs ajuda a executar como outro usuário e a validar compartilhamento. CRUD e FLS dependem das escolhas do código. Crie usuários e permissões demonstrativos, aplique o mecanismo de segurança adequado e afirme o resultado permitido e o bloqueado.
