Teste de Snapshot de Templates de E-mail com Verify
Quando Usar Este Skill
Use este skill quando:
- Testar a renderização de templates de e-mail para regressões
- Validar que templates MJML compilam para o HTML esperado
- Revisar alterações em e-mails durante code review (diffs são visuais)
- Garantir que a substituição de variáveis funcione corretamente
Skills relacionados:
aspnetcore/mjml-email-templates- Criação de templates MJMLaspire/mailpit-integration- Testar envio de e-mail localmentetesting/snapshot-testing- Padrões gerais do Verify
Por que Testar E-mails com Snapshot?
Templates de e-mail são:
- Visuais - Pequenas alterações podem quebrar a renderização em diferentes clientes
- Difíceis de testar unitariamente - A saída é HTML complexo, não valores simples
- Propensos a regressões - Alterações no template podem ter efeitos não intencionais
O teste de snapshot captura o HTML renderizado e falha quando ele muda inesperadamente.
Instalação
dotnet add package Verify.Xunit # ou Verify.NUnit, Verify.MSTest
Teste Básico de Snapshot de E-mail
[Fact]
public async Task UserSignupInvitation_RendersCorrectly()
{
// Arrange
var renderer = _services.GetRequiredService<IMjmlTemplateRenderer>();
var variables = new Dictionary<string, string>
{
{ "PreviewText", "Você foi convidado para participar da Acme Corp" },
{ "OrganizationName", "Acme Corporation" },
{ "InviteeName", "João Silva" },
{ "InviterName", "Ana Admin" },
{ "InvitationLink", "https://exemplo.com/invite/abc123" },
{ "ExpirationDate", "31 de dezembro de 2025" }
};
// Act
var html = await renderer.RenderTemplateAsync(
"UserInvitations/UserSignupInvitation",
variables);
// Assert
await Verify(html, extension: "html");
}
Isso cria UserSignupInvitation_RendersCorrectly.verified.html na primeira execução.
Revisando Alterações em E-mails
Quando um template muda, o teste falha com um diff. Opções de revisão:
1. Ferramenta de Diff Visual
# Configurar ferramenta de diff (uma vez)
dotnet tool install -g verify.tool
verify accept # Aceitar todas as alterações pendentes
verify review # Abrir ferramenta de diff
2. Visualização no Navegador
Abra o arquivo .received.html em um navegador para ver a renderização real.
3. Integração com IDE
A maioria das IDEs mostra diffs inline para arquivos .verified.html vs .received.html.
Testar Cada Variante do Template
Crie testes para cada template de e-mail para capturar regressões:
public class EmailTemplateSnapshotTests : IClassFixture<EmailTestFixture>
{
private readonly IMjmlTemplateRenderer _renderer;
public EmailTemplateSnapshotTests(EmailTestFixture fixture)
{
_renderer = fixture.Services.GetRequiredService<IMjmlTemplateRenderer>();
}
[Fact]
public async Task WelcomeEmail_NewUser() =>
await VerifyTemplate("Welcome/NewUser", new Dictionary<string, string>
{
{ "UserName", "João Silva" },
{ "LoginUrl", "https://exemplo.com/login" }
});
[Fact]
public async Task WelcomeEmail_InvitedUser() =>
await VerifyTemplate("Welcome/InvitedUser", new Dictionary<string, string>
{
{ "UserName", "João Silva" },
{ "InviterName", "Ana Admin" },
{ "OrganizationName", "Acme Corp" }
});
[Fact]
public async Task PasswordReset() =>
await VerifyTemplate("PasswordReset/PasswordReset", new Dictionary<string, string>
{
{ "UserName", "João Silva" },
{ "ResetLink", "https://exemplo.com/reset/abc123" },
{ "ExpirationMinutes", "30" }
});
[Fact]
public async Task PaymentReceipt() =>
await VerifyTemplate("Billing/PaymentReceipt", new Dictionary<string, string>
{
{ "UserName", "João Silva" },
{ "Amount", "R$ 10,00" },
{ "InvoiceNumber", "FAT-2025-001" },
{ "Date", "15 de janeiro de 2025" }
});
private async Task VerifyTemplate(
string templateName,
Dictionary<string, string> variables)
{
var html = await _renderer.RenderTemplateAsync(templateName, variables);
await Verify(html, extension: "html")
.UseMethodName(templateName.Replace("/", "_"));
}
}
Limpeza de Valores Dinâmicos
Alguns valores mudam entre execuções de teste. Limpe-os:
[Fact]
public async Task EmailWithTimestamp_ScrubsDynamicValues()
{
var html = await _renderer.RenderTemplateAsync("Welcome", variables);
await Verify(html, extension: "html")
.ScrubLinesContaining("Generated at:")
.ScrubInlineGuids(); // Limpa GUIDs em URLs
}
Limpadores Comuns
// Limpar datas
.ScrubLinesContaining("Date:")
.AddScrubber(s => Regex.Replace(s, @"\d{4}-\d{2}-\d{2}", "SCRUBBED-DATE"))
// Limpar URLs com tokens
.AddScrubber(s => Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"))
// Limpar GUIDs
.ScrubInlineGuids()
Fixture de Teste para Testes de E-mail
public class EmailTestFixture : IAsyncLifetime
{
public IServiceProvider Services { get; private set; } = null!;
public async Task InitializeAsync()
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SiteUrl"] = "https://exemplo.com"
})
.Build());
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
Services = services.BuildServiceProvider();
await Task.CompletedTask;
}
public Task DisposeAsync() => Task.CompletedTask;
}
Testes de Snapshot do Composer
Teste a saída completa do composer, incluindo assunto e metadados:
[Fact]
public async Task SignupInvitation_ComposesCorrectEmail()
{
var composer = _services.GetRequiredService<IUserEmailComposer>();
var email = await composer.ComposeSignupInvitationAsync(
recipientEmail: new EmailAddress("[email protected]"),
recipientName: new PersonName("João Silva"),
inviterName: new PersonName("Ana Admin"),
organizationName: new OrganizationName("Acme Corp"),
invitationUrl: new AbsoluteUri("https://exemplo.com/invite/abc123"),
expiresAt: new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero));
// Verificar o objeto de e-mail completo (assunto, para, corpo)
await Verify(new
{
email.To,
email.Subject,
HtmlBody = email.HtmlBody // Será armazenado com extensão .html
});
}
Integração com CI
Falhar se Linha de Base Ausente
No CI, falhe se nenhum arquivo .verified.html existir (evita aceitação acidental):
// Na configuração do teste ou ModuleInitializer
VerifierSettings.ThrowOnMissingVerifiedFile();
Configuração do Git
Adicione ao .gitattributes para melhorar o tratamento de diffs:
*.verified.html linguist-language=HTML
*.verified.html diff=html
Melhores Práticas
FAÇA
// FAÇA: Teste cada variante do template
[Fact] Task WelcomeEmail_NewUser_RendersCorrectly()
[Fact] Task WelcomeEmail_InvitedUser_RendersCorrectly()
// FAÇA: Use nomes de teste descritivos
[Fact] Task PaymentReceipt_WithRefund_ShowsRefundAmount()
// FAÇA: Limpe valores dinâmicos de forma consistente
.ScrubLinesContaining("Generated at:")
// FAÇA: Revise diffs cuidadosamente antes de aceitar
verify review
NÃO FAÇA
// NÃO FAÇA: Pule testes de e-mail
// NÃO FAÇA: Aceite alterações automaticamente sem revisão
verify accept --all // Perigoso!
// NÃO FAÇA: Teste apenas o caminho feliz
// NÃO FAÇA: Ignore falhas em testes de snapshot
Fluxo de Trabalho
- Criar template - Escreva o template MJML
- Escrever teste - Adicione teste de snapshot com variáveis de exemplo
- Executar teste - A primeira execução cria
.verified.html - Revisar - Abra no navegador, verifique a renderização
- Commitar - Inclua
.verified.htmlno controle de versão - Iterar - Alterações falham no teste, revise o diff, aceite se correto
Recursos
- Verify: https://github.com/VerifyTests/Verify
- Verify.Xunit: https://github.com/VerifyTests/Verify#xunit
- Diff Tools: https://github.com/VerifyTests/DiffEngine