Aprenda a montar fixtures de teste de validação de e-mail que cobrem erros de digitação, domínios mortos, catch-all e e-mails descartáveis, e como automatizá-los no CI.

Comece normalizando a entrada e testando os casos reais que os usuários submetem: espaços no início/fim, domínios em maiúsculas, pontos finais extras e plus tags. Depois adicione fixtures que cubram existência de domínio, presença de MX, comportamento catch-all e detecção de endereços descartáveis para que seus testes reflitam os riscos reais do cadastro.
Uma regex apenas diz que a string parece um e-mail. Ela não confirma se o domínio existe, se pode receber e-mails (registros MX) ou se é de um provedor descartável, então você pode acabar aprovando endereços que vão retornar bounce ou atrair SPAM.
Divida a validação em camadas que você possa testar separadamente: sintaxe e normalização (offline), sinais de domínio e MX (fronteira de rede), tipo de provedor (descartável vs normal) e suas regras de risco de negócio. Isso facilita diagnosticar falhas, pois cada fixture deve ser capturada por uma camada específica.
Use resultados graduados como valid, risky, invalid e unknown em vez de um único passa/falha. Domínios catch-all e timeouts são os grandes motivos: muitas vezes não é possível provar a entregabilidade, então um resultado “risky/unknown” permite que o produto decida entre permitir, avisar ou exigir verificação.
Inclua erros de digitação e formatação, domínios mortos (NXDOMAIN ou sem MX), casos catch-all, provedores descartáveis e lookalikes, além de padrões baseados em função ou suspeitos como admin@ ou longas sequências numéricas. Vincule cada fixture a uma decisão esperada para testar comportamento, não apenas parsing de strings.
Trate-os como registros estruturados: e-mail de entrada, resultado esperado, código de motivo e notas. Acrescente tags como typo, dead_domain, disposable ou catch_all para poder executar apenas o subconjunto relevante quando mudar uma regra.
Faça os testes unitários totalmente offline mockando resultados de DNS/MX/API, e grave ou reproduza respostas para os testes de contrato. Dependências da internet mudam com o tempo, então se você usá-las em CI terá falhas aleatórias não relacionadas ao seu código.
Mock a fronteira do validador para que o fluxo de cadastro seja testado de ponta a ponta com respostas fixas, incluindo timeouts e erros. Mantenha um job pequeno separado (noturno ou manual) que faça checagens reais para detectar deriva sem quebrar cada PR.
Trate-o como um sinal, não como prova, e afirme um resultado ambíguo como “domínio OK, caixa desconhecida” em vez de aprovar automaticamente. Na lógica do produto, um bom padrão é permitir o cadastro mas exigir confirmação ou aplicar limites; seus testes devem fixar essa política.
Trave resultados estruturados esperados por fixture e atualize-os apenas quando você mudar a política intencionalmente. Se usar Verimail, mapeie fixtures para os sinais do pipeline (syntax, domain, MX, disposable/blocklist) e mantenha o CI determinístico mockando respostas, enquanto roda periodicamente um conjunto real para detectar mudanças de classificação.
A maioria das suítes de teste de validação de e-mail parece sólida e ainda assim falha em produção porque endereços reais são bagunçados. Pessoas colam espaços em branco, usam plus tags, misturam maiúsculas, adicionam pontos finais, ou digitam rápido no celular e erram um caractere. Se seus fixtures só cobrem e-mails limpos e “didáticos”, você acaba testando um mundo onde seus usuários não vivem.
Outro problema comum é testar apenas a camada mais fácil: sintaxe. Verificações de sintaxe pegam erros óbvios, mas muitos cadastros ruins usam endereços com aparência válida em domínios que não existem, domínios sem configuração de e-mail, ou provedores descartáveis que mudam constantemente. Uma suíte que para num regex cria uma falsa sensação de segurança.
O objetivo não é bloquear tudo que pareça suspeito. O objetivo é impedir cadastros ruins sem bloquear pessoas reais. Isso significa testar os dois lados: rejeitar o que realmente precisa ser rejeitado e aceitar padrões legítimos e comuns (como plus addressing e subdomínios).
Esses padrões de falha aparecem repetidas vezes:
Bons testes cobrem camadas que combinam com como a validação funciona em cadastros reais: sintaxe (regras estilo RFC), sinais de domínio (domínio existe, registros MX), sinais de caixa postal (comportamento catch-all ou padrões de aceitação) e sinais de risco (blocklists e detecção de descartáveis).
Defina expectativas cedo: alguns resultados são probabilísticos. Domínios catch-all podem aceitar qualquer endereço sem provar que a caixa existe. Listas de descartáveis evoluem diariamente. Até ferramentas de nível empresarial como Verimail podem retornar sinais arriscados que são melhor tratados com regras de produto (permitir, bloquear ou exigir verificação extra), em vez de fingir que todo caso tem uma resposta perfeita.
Validação de e-mail parece uma checagem única, mas na verdade é uma cadeia de checagens menores. Separe essa cadeia em camadas e seus testes ficam mais claros e fáceis de manter. Isso também ajuda a construir fixtures que correspondam a cadastros reais em vez de strings aleatórias.
Um modelo prático de camadas fica assim:
Nem toda camada deve ser tratada da mesma forma nos testes. Algumas camadas são pura lógica e podem rodar rápido e offline (sintaxe, a maioria das regras de risco). Outras dependem da rede (DNS, lookup MX, listas em tempo real). Trate essas de forma diferente para que sua suíte de testes permaneça estável.
Em seguida, decida o que seu app faz com cada resultado de camada. Muitas equipes usam apenas passa ou falha, e acabam com casos de borda confusos. Um resultado graduado costuma ser mais fácil de trabalhar:
Um exemplo prático: um usuário digita [email protected]. A sintaxe passa, mas suas regras de sugestão de digitação o marcam como arriscado. Você pode permitir o cadastro, mas pedir que confirmem o endereço. Se entra [email protected], a sintaxe passa mas domínio ou MX falham, então você bloqueia.
Ao escrever testes, mapeie cada fixture para a camada que deve capturá-la e para o resultado esperado. Para camadas de rede, prefira testes de estilo boundary (por exemplo, como você lida com uma falha de DNS vs um resultado verdadeiro de no-MX) e mantenha o resto como testes unitários rápidos. Se você usa uma API como Verimail, pode espelhar seus resultados multiestágio (sintaxe, domínio, MX, checagem de descartáveis) nas suas próprias expectativas de camada.
Bons fixtures de validação de e-mail se parecem com o que usuários reais digitam, não com strings aleatórias. Se você construir fixtures em torno de algumas categorias claras de cenário, os testes ficam legíveis e as lacunas óbvias.
Mantenha estas categorias principais em toda suíte:
@, @@ duplo, espaços no início/fim, pontos duplos, um ponto logo antes do @, e caracteres trocados em domínios comuns. Estes devem falhar rapidamente na camada de sintaxe antes de qualquer checagem de rede.admin@, support@, info@ e padrões suspeitos como longas strings numéricas. Teste sua decisão de política (permitir, avisar ou bloquear), em vez de assumir que são sempre ruins.Para manter as categorias úteis, vincule cada fixture a uma decisão esperada, não apenas válido/inválido. Rótulos como syntax_invalid, domain_invalid, disposable_block, risky_allow_with_warning, unknown_catch_all tornam falhas mais fáceis de interpretar.
Se você usa um validador/API como Verimail, mapeie essas categorias para os estágios do pipeline que você espera disparar (sintaxe, checagens de domínio, lookup MX, correspondência em listas de descartáveis). Assim um teste que falha diz que tipo de falha ocorreu, não apenas que algo quebrou.
Bons fixtures parecem cadastros reais, não um amontoado de teclas. Quando um teste falha, você deve entender imediatamente o que aconteceu e por que importa.
Trate fixtures como pequenos registros com uma forma consistente: o e-mail de entrada, o resultado esperado (aceitar, rejeitar ou exigir checagens extras) e uma razão curta. Adicione um campo de notas para qualquer coisa que você esqueça no futuro (por que o domínio foi usado, ou qual comportamento você quer se o provedor mudar).
Aqui está um exemplo compacto que você pode copiar para seu repositório:
[
{
"name": "typo_gmail_missing_dot",
"input": "alex@gmailcom",
"expected": "reject",
"reason": "typo",
"tags": ["typo"],
"notes": "Missing dot in domain; should fail domain validation."
},
{
"name": "disposable_known_provider",
"input": "[email protected]",
"expected": "reject",
"reason": "disposable",
"tags": ["disposable"],
"notes": "Should be blocked by disposable provider list."
}
]
Tags facilitam o crescimento da suíte. Em vez de um arquivo gigante, mantenha pequenos conjuntos nomeados por categoria (typos, dead domains, catch-all, disposable, edge cases). Assim você pode rodar só o que precisa, por exemplo apenas casos descartáveis quando atualizar regras de classificação.
Regras de normalização pertencem aos fixtures também, porque usuários reais colam dados bagunçados. Inclua casos que provem que você trata:
Versione fixtures como código. Cada novo caso deve responder a uma pergunta: qual bug isso evitou? Adicione uma nota curta, exija revisão em mudanças e remova duplicatas. Com o tempo, seus fixtures viram um mapa vivo dos erros e ataques que seu cadastro realmente vê.
Se você depende de uma resposta de API (por exemplo, um serviço como Verimail), armazene um resultado esperado fixo por fixture e atualize-o apenas quando mudar a política intencionalmente.
Checagens de e-mail tocam em coisas que mudam sem aviso: registros DNS, configurações de servidor de e-mail e o ecossistema de provedores descartáveis. Se seus fixtures dependem da internet ao vivo, testes vão falhar numa terça-feira aleatória por motivos que nada têm a ver com seu código.
Comece separando o que dá para tornar determinístico do que não dá. Casos de sintaxe são fáceis: nunca devem requerer chamada de rede. A parte complicada é qualquer coisa que envolva alcance de domínio ou caixa postal.
Um conjunto estável de fixtures geralmente vem de algumas fontes:
Evite usar endereços de pessoas reais nos fixtures, mesmo se forem públicos. Use partes locais sintéticas (como "user" ou "test") e nomes claramente falsos. Isso reduz risco de privacidade e evita envios acidentais se dados de teste vazarem para logs ou sistemas downstream.
Para cenários de domínio-morto, a abordagem mais estável é tratar a falha DNS como dado de teste, não como realidade ao vivo. Grave ou faça mock do resultado do resolvedor (por exemplo, NXDOMAIN ou sem registros MX) e afirme que sua lógica lida com isso corretamente. Domínios mortos ao vivo são instáveis porque domínios podem ser comprados e configurados depois.
Detecção de descartáveis precisa de cuidado especial. Provedores aparecem, desaparecem e mudam domínios com frequência. Mantenha um snapshot versionado da sua lista de descartáveis (ou das respostas de classificação do seu vendedor de validação) e decida como lidar com a deriva. Por exemplo: testes unitários rodam contra o snapshot, enquanto um job agendado verifica mudanças e abre uma tarefa de revisão.
Escreva quais fixtures são sensíveis ao tempo e o que você fará quando mudarem: atualizar o snapshot, afrouxar a asserção ou mover o caso para um teste de contrato. Se você usa Verimail em produção, trate sua classificação em tempo real como algo que você testa com gravações fixas, depois verifique periodicamente com uma suíte pequena e controlada ao vivo.
Testes rápidos e confiáveis começam com as partes da validação que não tocam a rede. Trate seu validador como um conjunto de funções puras (entrada entra, saída sai) e em seguida trave o contrato do que o resto da sua app pode esperar.
Testes unitários devem cobrir regras de sintaxe e normalização, porque elas são fáceis de quebrar com pequenos refatores. Exemplos incluem trim de espaços, lowercase no domínio, rejeitar @@ duplo e lidar com erros óbvios como falta de ponto no domínio.
Ao construir fixtures, faça cada linha se explicar: entrada, valor normalizado esperado (ou vazio) e um código de razão curto. Códigos de razão são mais fáceis de afirmar do que frases completas.
Um padrão simples é testes dirigidos por tabela: iterar por uma tabela de fixtures e afirmar tanto status quanto motivo para cada caso. Isso mantém o arquivo de teste curto e cobre muitos cenários.
Testes de contrato respondem a outra pergunta: seu módulo de validação sempre retorna a mesma estrutura? Se uma equipe espera { status, reason, normalized } e outra release muda isso silenciosamente, você terá bugs que parecem aleatórios.
Testes de contrato devem verificar coisas como:
status, reason, normalized_email)valid, invalid, risky)Testes baseados em propriedade também ajudam aqui. Em vez de escrever manualmente cada typo, gere near-misses: espaços extras, caracteres trocados em torno do @, pontos repetidos, ou domínios com maiúsculas misturadas. O objetivo é pegar bugs do parser e casos de borda que você não pensou em incluir.
Snapshot testing pode ajudar para mensagens visíveis na UI, mas use com cuidado. Prefira snapshot de códigos de erro estáveis, não de textos completos. Se precisar travar mensagens, mantenha-as curtas e consistentes para que pequenas edições de cópia não quebrem metade da suíte.
Testes de integração são onde a lógica de validação de e-mail frequentemente vira algo não confiável. No momento em que um teste depende de DNS real, lookups MX ou de um serviço de terceiros disponível, você pode ter falhas aleatórias que nada têm a ver com seu código.
Para execuções diárias de CI, mire em testes rápidos e repetíveis. Trate a rede como uma entrada que você controla.
Uma abordagem prática é mockar a fronteira de rede e focar no que seu app faz com cada resultado. Se seu serviço de cadastro chama uma API de validação de e-mail, substitua essa chamada por um stub que retorna respostas conhecidas para suas fixtures. Você ainda testa o fluxo completo (controller, serviço, decisão de política, mensagem de erro), mas não testa a internet.
Padrões que tendem a funcionar bem:
Timeouts merecem atenção especial. Decida sua política e teste-a explicitamente: quando a validação expira, você trata o e-mail como inválido, ou como desconhecido e permite que o usuário prossiga com verificação extra? Ambos podem estar corretos, mas só se for consistente.
Testes mockados podem divergir da realidade. Um domínio que tinha registros MX pode ficar morto. Uma classificação como descartável pode mudar. Para detectar deriva, mantenha um job separado que faça chamadas reais, mas não rode isso em todo pull request.
Uma configuração típica é uma execução noturna ou um fluxo manual de checagens ao vivo. Mantenha essa trilha pequena: um punhado de e-mails representativos por categoria (typos, dead domains, catch-all, disposable) é suficiente para detectar deriva sem gerar ruído.
Se você usa Verimail em produção, seus testes CI podem stubar a resposta disposable: true para uma fixture como [email protected], e você afirma que sua UI bloqueia o cadastro e mostra a mensagem certa. Então seu job noturno pode chamar a API real com um conjunto controlado de endereços e avisar se os resultados mudarem, para que você possa atualizar fixtures ou ajustar política antes que os usuários percebam.
Uma suíte de testes pode parecer ocupada e ainda assim perder falhas que te prejudicam em produção. O maior risco é a confiança falsa: os testes passam, mas usuários reais são bloqueados, ou cadastros falsos passam.
Uma armadilha comum é tratar catch-all como prova de que a caixa existe. Catch-all só significa que o domínio aceita e-mail para qualquer endereço. Se sua lógica aprovar automaticamente tudo em um catch-all, seus fixtures te ensinam a aceitar endereços lixo.
Outra armadilha é usar apenas um regex e chamar isso de validação. Regex pega problemas de formatação óbvios, mas não diz se o domínio existe, se tem registros MX, ou se o endereço é de um provedor descartável. Se seus testes só cobrem padrões de string, você está testando o regex, não o comportamento de e-mail.
Falhar usuários por problemas temporários de DNS também é um erro frequente. Redes reais têm timeouts e falhas intermitentes. Se seus testes só cobrem DNS funcionando vs DNS falhando, você pode acabar rejeitando bons usuários durante uma breve interrupção. Uma abordagem melhor é tratar alguns erros como desconhecidos e tentar de novo, ou permitir o cadastro mas marcar o endereço para verificação posterior.
Domínios internacionalizados e regras modernas de caixas são fáceis de esquecer. Plus-addressing (como [email protected]) é válido e amplamente usado. Alguns usuários também têm domínios não-ASCII. Se seus fixtures nunca incluem isso, você vai lançar um validador que quebra em entradas normais.
Algumas formas de equipes se iludirem:
+ é inválido ou removê-lo incorretamente.Um salvaguarda prática é separar resultados em buckets claros (invalid, risky, unknown, valid) e testar transições entre eles. Ferramentas como Verimail ajudam retornando sinais estruturados (syntax, domain, MX, disposable), o que facilita afirmar comportamento sem adivinhar a partir de um único passa/falha.
Antes de fazer merge, faça uma passada rápida por cobertura e confiança. O objetivo não é testar todo e-mail do mundo, mas garantir que fixtures se comportem como cadastros reais e falhem de forma previsível.
Revise seu conjunto de fixtures por categoria. Se uma categoria só tem um ou dois exemplos, uma pequena mudança de código pode quebrar usuários reais e seus testes ainda passarem. Mire num pequeno cluster de casos por categoria para pegar bordas (por exemplo, um typo que ainda parece válido).
Use esta checklist curta:
Não ignore estabilidade. Testes que dependem de domínios reais podem apodrecer com o tempo, e comportamento catch-all pode mudar sem aviso. Para testes unitários, prefira fixtures que não requerem a rede e mantenha comportamento de domínio como respostas mockadas. Para testes de integração, limite o escopo e torne-os intencionais. Um padrão simples é um job noturno pequeno que exercita alguns casos conhecidos contra seu serviço de validação (ou provedor), enquanto o CI foca em testes unitários determinísticos.
Um problema comum em SaaS: seu formulário de cadastro parece ok em testes manuais, mas depois do lançamento você começa a ver ondas de cadastros de spam. Muitos usam provedores descartáveis, o que vira leads ruins, mais bounces e um banco de dados de usuários bagunçado.
Uma abordagem prática é definir comportamento claro e travá-lo com fixtures. Por exemplo: bloquear endereços descartáveis, rejeitar domínios mortos e permitir (mas avisar sobre) domínios catch-all.
Aqui está um pequeno conjunto de fixtures que cobre entradas realistas de cadastro sem depender de strings aleatórias:
[email protected] (parece real, mas o domínio está digitado errado)[email protected] (use .invalid para representar um domínio que nunca deve resolver)[email protected] (trate isto como catch-all nos mocks, não como fato real de DNS)[email protected] (trate isto como descartável nos mocks, não como um provedor real)[email protected] (um endereço com aparência normal para o happy path)O ponto é que seu app não deve tentar provar catch-all ou status descartável usando a internet pública durante testes unitários. Em vez disso, sua camada de validação deve retornar um resultado normalizado que a lógica de cadastro possa usar.
Uma regra simples para o endpoint de cadastro pode ser:
is_disposable = true: bloquear cadastro com erro clarodomain_status = dead: rejeitar e pedir outro e-mailis_catch_all = true: permitir cadastro, mas mostrar aviso (e considerar verificação extra)Para manter o CI rápido e previsível, divida testes em duas velocidades: testes unitários rápidos para sua lógica de decisão, e testes de integração mockados para a fronteira do validador.
// Example: decision logic unit tests (no network)
const cases = [
{ email: "[email protected]", result: { is_disposable: true }, expect: "BLOCK" },
{ email: "[email protected]", result: { domain_status: "dead" }, expect: "REJECT" },
{ email: "[email protected]", result: { is_catch_all: true }, expect: "WARN" },
{ email: "[email protected]", result: { suggestion: "gmail.com" }, expect: "REJECT" },
{ email: "[email protected]", result: { ok: true }, expect: "ALLOW" },
];
for (const c of cases) {
const decision = decideSignup(c.email, c.result);
expect(decision).toBe(c.expect);
}
No CI, o teste de integração mockado pode verificar que seu código de cadastro chama o validador uma vez e lida com timeouts e respostas de erro, sem realmente fazer lookups DNS ou MX.
Se você quer checagens reais (sintaxe compatível com RFC, verificação de domínio, lookup MX e correspondência em listas de descartáveis/blocklists) sem construir e manter essa lógica internamente, uma API de validação de e-mail pode ser uma boa opção. Para equipes que já usam Verimail, um padrão simples é manter a maioria dos testes mockados e determinísticos, e então rodar um pequeno conjunto de checagens de contrato para garantir que sua integração com verimail.co ainda corresponda à estrutura de resposta que seu app espera.