| name | verify-email-snapshots |
| description | Snapshot test email templates using Verify to catch regressions. Validates rendered HTML output matches approved baseline. Works with MJML templates and any email renderer. |
| invocable | false |
Snapshot Testing Email Templates with Verify
When to Use This Skill
Use this skill when:
- Testing email template rendering for regressions
- Validating MJML templates compile to expected HTML
- Reviewing email changes in code review (diffs are visual)
- Ensuring variable substitution works correctly
Related skills:
aspnetcore/mjml-email-templates - MJML template authoring
aspire/mailpit-integration - Test email delivery locally
testing/snapshot-testing - General Verify patterns
Why Snapshot Test Emails?
Email templates are:
- Visual - Small changes can break rendering across clients
- Hard to unit test - Output is complex HTML, not simple values
- Prone to regression - Template changes can have unintended effects
Snapshot testing captures the rendered HTML and fails when it changes unexpectedly.
Installation
dotnet add package Verify.Xunit
Basic Email Snapshot Test
[Fact]
public async Task UserSignupInvitation_RendersCorrectly()
{
var renderer = _services.GetRequiredService<IMjmlTemplateRenderer>();
var variables = new Dictionary<string, string>
{
{ "PreviewText", "You've been invited to join Acme Corp" },
{ "OrganizationName", "Acme Corporation" },
{ "InviteeName", "John Doe" },
{ "InviterName", "Jane Admin" },
{ "InvitationLink", "https://example.com/invite/abc123" },
{ "ExpirationDate", "December 31, 2025" }
};
var html = await renderer.RenderTemplateAsync(
"UserInvitations/UserSignupInvitation",
variables);
await Verify(html, extension: "html");
}
This creates UserSignupInvitation_RendersCorrectly.verified.html on first run.
Reviewing Email Changes
When a template changes, the test fails with a diff. Review options:
1. Visual Diff Tool
dotnet tool install -g verify.tool
verify accept
verify review
2. Browser Preview
Open the .received.html file in a browser to see the actual rendering.
3. IDE Integration
Most IDEs show inline diffs for .verified.html vs .received.html files.
Test Each Template Variant
Create tests for each email template to catch regressions:
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", "John Doe" },
{ "LoginUrl", "https://example.com/login" }
});
[Fact]
public async Task WelcomeEmail_InvitedUser() =>
await VerifyTemplate("Welcome/InvitedUser", new Dictionary<string, string>
{
{ "UserName", "John Doe" },
{ "InviterName", "Jane Admin" },
{ "OrganizationName", "Acme Corp" }
});
[Fact]
public async Task PasswordReset() =>
await VerifyTemplate("PasswordReset/PasswordReset", new Dictionary<string, string>
{
{ "UserName", "John Doe" },
{ "ResetLink", "https://example.com/reset/abc123" },
{ "ExpirationMinutes", "30" }
});
[Fact]
public async Task PaymentReceipt() =>
await VerifyTemplate("Billing/PaymentReceipt", new Dictionary<string, string>
{
{ "UserName", "John Doe" },
{ "Amount", "$10.00" },
{ "InvoiceNumber", "INV-2025-001" },
{ "Date", "January 15, 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("/", "_"));
}
}
Scrubbing Dynamic Values
Some values change between test runs. Scrub them:
[Fact]
public async Task EmailWithTimestamp_ScrubsDynamicValues()
{
var html = await _renderer.RenderTemplateAsync("Welcome", variables);
await Verify(html, extension: "html")
.ScrubLinesContaining("Generated at:")
.ScrubInlineGuids();
}
Common Scrubbers
.ScrubLinesContaining("Date:")
.AddScrubber(s => Regex.Replace(s, @"\d{4}-\d{2}-\d{2}", "SCRUBBED-DATE"))
.AddScrubber(s => Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"))
.ScrubInlineGuids()
Test Fixture for Email Tests
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://example.com"
})
.Build());
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
Services = services.BuildServiceProvider();
await Task.CompletedTask;
}
public Task DisposeAsync() => Task.CompletedTask;
}
Composer Snapshot Tests
Test the full composer output including subject and metadata:
[Fact]
public async Task SignupInvitation_ComposesCorrectEmail()
{
var composer = _services.GetRequiredService<IUserEmailComposer>();
var email = await composer.ComposeSignupInvitationAsync(
recipientEmail: new EmailAddress("john@example.com"),
recipientName: new PersonName("John Doe"),
inviterName: new PersonName("Jane Admin"),
organizationName: new OrganizationName("Acme Corp"),
invitationUrl: new AbsoluteUri("https://example.com/invite/abc123"),
expiresAt: new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero));
await Verify(new
{
email.To,
email.Subject,
HtmlBody = email.HtmlBody
});
}
CI Integration
Fail on Missing Baseline
In CI, fail if no .verified.html file exists (prevents accidental acceptance):
VerifierSettings.ThrowOnMissingVerifiedFile();
Git Configuration
Add to .gitattributes to improve diff handling:
*.verified.html linguist-language=HTML
*.verified.html diff=html
Best Practices
DO
[Fact] Task WelcomeEmail_NewUser_RendersCorrectly()
[Fact] Task WelcomeEmail_InvitedUser_RendersCorrectly()
[Fact] Task PaymentReceipt_WithRefund_ShowsRefundAmount()
.ScrubLinesContaining("Generated at:")
verify review
DON'T
verify accept --all
Workflow
- Create template - Write MJML template
- Write test - Add snapshot test with sample variables
- Run test - First run creates
.verified.html
- Review - Open in browser, verify rendering
- Commit - Include
.verified.html in source control
- Iterate - Changes fail test, review diff, accept if correct
Resources