Skip to content
Development
Skill

/verify-email-snapshots

Snapshot test email templates using Verify to catch regressions. Validates rendered HTML output matches approved baseline. Works with MJML templates and any email renderer.

From plugin
dotnet-skills
1.2k39 skills6 agents

Claude Code

$ npx -y skills add aaronontheweb/dotnet-skills --skill verify-email-snapshots --agent claude-code

SKILL.md

verify-email-snapshots.SKILL.md
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.
user-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: 1. **Visual** - Small changes can break rendering across clients 2. **Hard to unit test** - Output is complex HTML, not simple values 3. **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  # or Verify.NUnit, Verify.MSTest

---

Basic Email Snapshot Test

[Fact]
public async Task UserSignupInvitation_RendersCorrectly()
{
    // Arrange
    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" }
    };

    // Act
    var html = await renderer.RenderTemplateAsync(
        "UserInvitations/UserSignupInvitation",
        variables);

    // Assert
    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

# Configure diff tool (one-time)
dotnet tool install -g verify.tool
verify accept  # Accept all pending changes
verify review  # Open diff tool

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();  // Scrubs GUIDs in URLs
}

Common Scrubbers

// Scrub dates
.ScrubLinesContaining("Date:")
.AddScrubber(s => Regex.Replace(s, @"\d{4}-\d{2}-\d{2}", "SCRUBBED-DATE"))

// Scrub URLs with tokens
.AddScrubber(s => Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"))

// Scrub GUIDs
.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:

Read more
@aaronontheweb
Ships withdotnet-skills

A comprehensive AI coding plugin with 30 skills and 5 specialized agents for professional .NET development. Battle-tested patterns from production systems covering C#, Akka.NET, Aspire, EF Core, testing, and performance optimization.

1.2k39 skills6 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
1.2k
Star history of aaronontheweb/dotnet-skills on GitHub, a chart by star-history.com

Other skills on dotnet-skills.

aot-trimming

aot-trimming

Guidelines for making .NET libraries and applications trimming-safe and Native AOT compatible. Covers the trimming/AOT model, the…

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills11d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about verify-email-snapshots.

Which coding agents can use verify-email-snapshots?
verify-email-snapshots installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.