Desenvolvimento

TestDataFactory no Apex: pare de duplicar a preparação dos testes

Ícones lineares de uma fábrica com engrenagem, uma pilha de registros, um frasco de laboratório e uma marca de verificação, ligados por uma linha

Uma classe de teste começa com uma Account e uma Opportunity criadas na mão. Seis meses depois, vinte classes repetem o mesmo arranjo com pequenas diferenças, e uma regra de validação nova derruba metade da suíte. A TestDataFactory resolve esse problema de manutenção, desde que você defina bem o que ela faz e o que fica no teste.

Uma TestDataFactory é uma classe @IsTest que concentra a criação dos registros usados pelos testes Apex. Cada teste pede uma Account ou Opportunity válida à factory e altera só o que interessa ao cenário. Quando um campo obrigatório muda, você ajusta um método em vez de vinte classes, e nenhum teste depende dos dados que já existem na org.

Onde a preparação duplicada cobra a conta

Uma classe de teste costuma começar criando Account, Contact e Opportunity dentro do próprio método. O cenário passa, a classe seguinte copia o arranjo e, em pouco tempo, cada teste carrega uma versão ligeiramente diferente da mesma preparação. Uma preenche Industry, outra esquece StageName, uma terceira usa CloseDate fixa em uma data que já passou.

A conta chega quando a org muda. Suponha que a Vetra Distribuidora, empresa fictícia das demonstrações deste blog, crie uma regra de validação exigindo Customer_Tier__c em toda conta ativa. Os testes que montam contas manualmente quebram em várias classes de uma vez, com FIELD_CUSTOM_VALIDATION_EXCEPTION. Nenhum desses testes encontrou defeito no código avaliado. A falha só mostra que a preparação estava espalhada.

Com uma factory, o método accountDefault sabe produzir uma conta mínima e válida, e opportunityDefault conhece os campos obrigatórios de uma oportunidade. A correção da regra nova vira uma linha. O teste continua livre para mudar o que interessa ao cenário, sem redescobrir tudo o que o insert exige.

Isso também segue a orientação da Salesforce sobre isolamento. O guia Isolation of Test Data from Organization Data in Unit Tests diz que testes salvos na API 24.0 ou posterior não enxergam os dados da org (com exceções como User, Profile e RecordType) e recomenda criar os dados dentro do teste. Os registros criados são revertidos no fim, e o resultado é o mesmo em scratch org, sandbox e produção.

Comece com construção previsível

A primeira versão cabe em uma classe curta, desde que deixe claro o que é valor padrão e o que é decisão do teste. O desenho mais simples devolve o registro sem inserir:

@IsTest
public class TestDataFactory {
    public static Account accountDefault() {
        return new Account(
            Name = 'Conta de teste',
            Industry = 'Technology'
        );
    }

    public static Opportunity opportunityDefault(Id accountId) {
        return new Opportunity(
            Name = 'Oportunidade de teste',
            AccountId = accountId,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30)
        );
    }
}

A anotação @IsTest tem dois efeitos documentados no Apex Developer Guide: a classe fica fora do limite de tamanho de código da org e só pode ser chamada por código de teste. Os métodos precisam ser public ou global para outras classes de teste os enxergarem.

O teste controla o DML. Isso ajuda quando você quer alterar um campo antes do insert, provar uma validação negativa ou juntar vários registros em uma única operação:

@IsTest
private class OpportunityServiceTest {
    @IsTest
    static void shouldCreateOpportunityForActiveAccount() {
        Account account = TestDataFactory.accountDefault();
        insert account;

        Opportunity opportunity =
            TestDataFactory.opportunityDefault(account.Id);

        Test.startTest();
        insert opportunity;
        Test.stopTest();

        Opportunity saved = [
            SELECT StageName, AccountId
            FROM Opportunity
            WHERE Id = :opportunity.Id
        ];
        Assert.areEqual(account.Id, saved.AccountId);
        Assert.areEqual('Prospecting', saved.StageName);
    }
}

O nome do método da factory descreve o dado entregue (accountDefault, opportunityDefault). Fuja de nomes como createEverythingForRenewalScenarioWithFiveProducts: quando a factory absorve a regra de negócio, ela vira mais uma classe difícil de ler. Quem narra o cenário é o teste.

Construção e DML em métodos diferentes

Muita equipe usa uma sobrecarga com Boolean doInsert. Funciona, mas tem um custo de leitura: TestDataFactory.accounts(5, true) executa DML sem que a palavra insert apareça na chamada. Eu prefiro dois métodos com nomes que não deixam dúvida:

public static Account insertAccountDefault() {
    Account record = accountDefault();
    insert record;
    return record;
}

O contrato importa mais do que a forma. Se métodos sem prefixo nunca inserem e métodos insert... sempre persistem, quem lê o teste sabe onde está cada DML. Se o time já adotou o booleano, mantenha. Como o Apex não tem argumento nomeado, passe uma variável local com nome explícito (Boolean persist = true; seguido de TestDataFactory.accounts(5, persist)) para o leitor não precisar abrir a factory.

Também não faça query na factory para buscar o registro que acabou de inserir. A instância já recebe o Id depois do DML; devolva a própria instância. Para volume, monte uma lista e insira de uma vez, com a mesma regra de bulkificação no Apex que vale para o código de produção:

public static List<Account> accounts(Integer quantity) {
    List<Account> records = new List<Account>();
    for (Integer index = 0; index < quantity; index++) {
        Account item = accountDefault();
        item.Name = 'Conta de teste ' + index;
        records.add(item);
    }
    return records;
}

Com isso, insert TestDataFactory.accounts(200); dispara o trigger com 200 registros em um único lote, o tamanho de chunk que a plataforma usa para triggers. Um teste com dois registros prova a regra funcional. O de 200 mostra se existe SOQL ou DML dentro de laço, porque o teste estoura System.LimitException antes da asserção.

Onde ficam as variações de cada cenário

Valores padrão reduzem ruído, mas não podem engessar os cenários. Em vez de uma sobrecarga para cada combinação, devolva o registro antes do DML e deixe o teste mudar só os campos que fazem parte da hipótese. No exemplo, Customer_Tier__c é o campo custom da Vetra fictícia:

Account account = TestDataFactory.accountDefault();
account.Customer_Tier__c = 'Strategic';
account.BillingCountry = 'Brazil';
insert account;

A regra para decidir onde cada campo mora é curta. Obrigatório em toda criação vai para o método padrão. Relevante para um caso só fica no teste, à vista de quem lê. Assim fica óbvio qual dado aciona o comportamento verificado.

Para relacionamentos, receba os IDs dos pais como parâmetro. Uma factory de Contact aceita accountId; um OpportunityLineItem precisa do ID da oportunidade e do PricebookEntryId. Não deixe a factory procurar "qualquer conta" no banco, porque isso esconde a dependência e torna o resultado ambíguo quando houver duas. O chamador monta o grafo em ordem visível.

Factory também não prova segurança. Se o comportamento depende de perfil, permission set ou compartilhamento, crie o usuário e rode o trecho em System.runAs. A análise de CRUD, FLS e sharing continua sendo parte do cenário; a factory só facilita criar os registros e o usuário.

Onde entra o @TestSetup

A factory é reutilizável entre classes. O método @TestSetup pertence a uma classe de teste e prepara a base comum aos métodos dela. Pela documentação de Using Test Setup Methods, ele roda uma vez, antes dos métodos da classe, e cada método recebe os registros no estado original: alterações feitas por um método são desfeitas antes do próximo. A classe aceita um único @TestSetup, e variáveis estáticas preenchidas nele não chegam aos métodos de teste.

@IsTest
private class OpportunityRulesTest {
    @TestSetup
    static void setupData() {
        Account account = TestDataFactory.accountDefault();
        insert account;

        Opportunity opportunity =
            TestDataFactory.opportunityDefault(account.Id);
        insert opportunity;
    }

    @IsTest
    static void shouldKeepDefaultStage() {
        Opportunity opportunity = [
            SELECT Id, StageName
            FROM Opportunity
            LIMIT 1
        ];
        Assert.areEqual('Prospecting', opportunity.StageName);
    }
}

A restrição das estáticas pega muita gente. Guardar o Id da conta numa variável estática no setup e ler no método devolve null. Os métodos precisam buscar os registros por SOQL, como no exemplo.

Use @TestSetup quando vários métodos precisam da mesma base de verdade. Carregar um catálogo inteiro "por garantia" deixa cada execução mais lenta e esconde de onde veio o estado consultado. Se só um método precisa de cinco produtos e uma cotação, ele cria esse complemento sozinho.

Peça Pergunta que responde Escopo
TestDataFactory Como construir um registro válido? Qualquer classe de teste
@TestSetup Que base estes métodos compartilham? Uma classe de teste
Método de teste Que comportamento estou provando? Um cenário

Quando as três perguntas se misturam na mesma classe, ela parece curta e fica opaca na primeira manutenção.

Test.startTest delimita o que está sendo medido

Crie os dados antes de Test.startTest(). Entre startTest e stopTest fica só a chamada ao código de produção. Pela documentação dos métodos da classe Test, esse bloco recebe um novo conjunto de limites de governador, cada método de teste pode chamar startTest uma vez só, e o trabalho assíncrono enfileirado dentro dele termina quando stopTest executa.

Não use essa janela para esconder uma preparação cara. Se a factory gasta tantas queries que o teste só passa porque os limites reiniciam, revise a factory. O código de produção precisa ser medido com um orçamento parecido com o real, ainda mais quando o alvo é um Queueable, Batch ou Schedulable.

O formato que funciona é Arrange, Act, Assert: factory e setup montam o estado, o bloco startTest/stopTest executa a ação, e as consultas no fim provam o resultado observável. Cobertura de linha não substitui esse terceiro passo. Um teste que chama o método e não confere nada pode bater os 75% exigidos para deploy em produção e deixar passar qualquer regressão.

Sinais de que a factory cresceu demais

Uma factory única que conhece vendas, atendimento, cobrança e integrações vira ponto de conflito em todo merge, e ninguém sabe qual valor padrão é seguro alterar. Quando ela perder coesão, divida por domínio (SalesTestDataFactory, ServiceTestDataFactory) e mantenha as mesmas convenções de nome nas duas.

Outro sinal é método com dez parâmetros posicionais. Trocar dois String de lugar compila e gera um registro errado sem aviso. Prefira o padrão válido com ajustes locais e, se a variação for grande de verdade, um builder pequeno. Eu só criaria o builder depois de ver o problema em três ou quatro testes, nunca antes.

Dados aleatórios também atrapalham. Crypto.getRandomInteger() no nome da conta garante unicidade, mas a falha deixa de ser reproduzível. Um contador estático na factory (private static Integer counter = 0; e 'Conta de teste ' + counter++) resolve a unicidade com valores que você consegue procurar no log. Lembre que o contexto estático é reiniciado a cada método de teste, inclusive depois do @TestSetup: se o nome precisa ser único também em relação aos registros do setup, acrescente um prefixo por cenário.

Por fim, a factory não é desculpa para SeeAllData=true. Um teste que depende dos registros atuais da org passa no sandbox e falha no deploy para produção, ou muda de resultado sem nenhuma alteração de código.

Como adotar sem reescrever a suíte

Comece pela classe que mais duplica preparação ou que mais quebra quando um metadado muda. Extraia a criação de um objeto central e não mexa nas asserções. Rode a classe antes e depois. Depois migre um segundo teste que usa o mesmo objeto e confira se o método padrão ainda serve aos dois.

No code review, olhe quatro coisas: o método padrão preenche só o necessário para um registro válido; a chamada deixa claro se há DML; o teste altera localmente os campos que explicam o cenário; e as asserções verificam o comportamento do código de produção.

Em seguida, acrescente um cenário de 200 registros para o fluxo mais importante e veja se a lógica segue o padrão do trigger handler. A factory deve deixar esse teste mais curto, sem mascarar as consultas e automações disparadas pelo insert. Quando a próxima regra de validação chegar, você corrige um método, roda a suíte e analisa só os testes cujo cenário mudou de fato.

Perguntas frequentes

O que é uma TestDataFactory no Salesforce?

É uma classe utilitária anotada com @IsTest que reúne métodos reutilizáveis para construir, e quando necessário inserir, os registros usados pelos testes Apex. Por ter @IsTest, ela não conta no limite de tamanho de código da org.

TestDataFactory substitui o método @TestSetup?

Não. A factory define como cada registro é construído e pode ser usada por qualquer classe de teste. O @TestSetup prepara uma base comum aos métodos de uma única classe. Os dois trabalham juntos.

Uma factory deve inserir os registros automaticamente?

Não em todos os métodos. Separar construção e persistência, ou deixar o DML explícito no nome do método, permite que cada teste decida quando o insert acontece e altere campos antes dele.

Por que evitar SeeAllData=true nos testes Apex?

Porque os dados existentes variam entre sandbox, scratch org e produção. Desde a API 24.0 os testes não enxergam os dados da org por padrão, e criar os próprios registros mantém a execução isolada e repetível.

Segunda opinião técnica

Sua org cresceu. Os riscos também?

Revisão independente de automações, integrações e pontos frágeis, com prioridades claras para decidir o próximo passo.

Conversar sobre o problema →