Skip to main content
skill CX Codex

verify-email-snapshots — Claude Code skill: snapshot test your emails

A skill that snapshot tests rendered HTML emails with Verify: output is compared against an approved baseline and the test fails on any change.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add Aaronontheweb/dotnet-skills --skill verify-email-snapshots -a claude-code
Open on GitHub

What verify-email-snapshots does

Reach for this when emails come out of MJML templates or another renderer and you want regressions caught. The agent writes xUnit, NUnit or MSTest tests that pass the rendered string into Verify(html, extension: "html"). The first run drops a .verified.html file next to the test as the baseline; later template edits produce a .received.html and a diff.

Dynamic values are the usual source of noise. The skill shows how to scrub them: ScrubLinesContaining, ScrubInlineGuids, or regex via AddScrubber for dates and token URLs. Give each template variant its own test and set the name with UseMethodName, otherwise the files overwrite each other.

You need the Verify.Xunit package (or Verify.NUnit, Verify.MSTest) plus a template renderer wired into the test's DI container. Reviewing diffs works in a browser or through verify.tool — verify review opens the comparison tool, verify accept takes the changes. For CI the skill suggests failing the test when no baseline file exists, so fresh output can't be accepted by accident.

What it helps with

  • catch a layout regression after editing an email template
  • check that MJML compiles to the expected HTML
  • confirm variable substitution still works
  • compare renderer output before and after a refactor
  • show an email change as a diff in code review

Example prompts for the agent

  • write a snapshot test for the UserInvitations/UserSignupInvitation template with Verify
  • add scrubbers for dates and GUIDs to the PasswordReset email test
  • snapshot the SignupInvitation composer output including subject and body

How to install verify-email-snapshots

1Skills CLI (Vercel)

npx skills add Aaronontheweb/dotnet-skills --skill verify-email-snapshots -a claude-code

2Manually: put the files in place

~/.claude/skills/verify-email-snapshots/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/verify-email-snapshots/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about verify-email-snapshots

How do I install verify-email-snapshots in Claude?

Run npx skills add Aaronontheweb/dotnet-skills --skill verify-email-snapshots -a claude-code. The skill lands in ~/.claude/skills/verify-email-snapshots/SKILL.md.

What does verify-email-snapshots do?

It writes snapshot tests for HTML emails: the rendered template is compared against an approved .verified.html baseline and the test fails when the output changes.

What do I need for the tests to run?

The Verify.Xunit package (or Verify.NUnit, Verify.MSTest) and a template renderer available from the test. Dynamic values need scrubbing.

Source text

SKILL.md · MIT
---
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

```bash
dotnet add package Verify.Xunit  # or Verify.NUnit, Verify.MSTest
```

---

## Basic Email Snapshot Test

```csharp
[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

```bash
# 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:

```csharp
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:

```csharp
[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

```csharp
// 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

```csharp
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:

```csharp
[Fact]
public async Task SignupInvitation_ComposesCorrectEmail()
{
    var composer = _services.GetRequiredService<IUserEmailComposer>();

    var email = await composer.ComposeSignupInvitationAsync(
        recipientEmail: new EmailAddress("[email protected]"),
        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));

    // Verify the full email object (subject, to, body)
    await Verify(new
    {
        email.To,
        email.Subject,
        HtmlBody = email.HtmlBody  // Will be stored as .html extension
    });
}
```

---

## CI Integration

### Fail on Missing Baseline

In CI, fail if no `.verified.html` file exists (prevents accidental acceptance):

```csharp
// In test setup or ModuleInitializer
VerifierSettings.ThrowOnMissingVerifiedFile();
```

### Git Configuration

Add to `.gitattributes` to improve diff handling:

```gitattributes
*.verified.html linguist-language=HTML
*.verified.html diff=html
```

---

## Best Practices

### DO

```csharp
// DO: Test each template variant
[Fact] Task WelcomeEmail_NewUser_RendersCorrectly()
[Fact] Task WelcomeEmail_InvitedUser_RendersCorrectly()

// DO: Use descriptive test names
[Fact] Task PaymentReceipt_WithRefund_ShowsRefundAmount()

// DO: Scrub dynamic values consistently
.ScrubLinesContaining("Generated at:")

// DO: Review diffs carefully before accepting
verify review
```

### DON'T

```csharp
// DON'T: Skip email testing
// DON'T: Auto-accept changes without review
verify accept --all  // Dangerous!

// DON'T: Test only happy path
// DON'T: Ignore snapshot test failures
```

---

## Workflow

1. **Create template** - Write MJML template
2. **Write test** - Add snapshot test with sample variables
3. **Run test** - First run creates `.verified.html`
4. **Review** - Open in browser, verify rendering
5. **Commit** - Include `.verified.html` in source control
6. **Iterate** - Changes fail test, review diff, accept if correct

---

## Resources

- **Verify**: https://github.com/VerifyTests/Verify
- **Verify.Xunit**: https://github.com/VerifyTests/Verify#xunit
- **Diff Tools**: https://github.com/VerifyTests/DiffEngine

© Aaronontheweb · MIT · Aaronontheweb/dotnet-skills

Similar skills

Everything for Testing and QA →

From the same set32

playwright-ci-caching Caches Playwright browser binaries in GitHub Actions and Azure DevOps pipelines. Eliminates the ~400MB download on every CI run, cutting 1-2 minutes off test workflows. CX Codex +1 434installs dotnet-devcert-trust Walks through why .NET and Aspire reject the HTTPS dev certificate on Linux and gets it into the system CA bundle. CX Codex +1 434installs akka-net-best-practices A skill covering the tricky parts of Akka.NET: EventStream vs DistributedPubSub, supervision, error handling. Useful when you design actors. CX Codex +1 431installs mailpit-integration Runs Mailpit inside .NET Aspire so outgoing email is captured locally instead of delivered. Useful when you debug templates or assert on mail in integration tests. CX Codex +1 421installs akka-hosting-actor-patterns Patterns for building Akka.NET entity actors with Akka.Hosting. Useful when you need actors that run in both lightweight local unit tests and clustered production environments. CX Codex +1 414installs akka-net-testing-patterns A skill for writing unit and integration tests for Akka.NET actors with Akka.Hosting.TestKit: dependency injection, TestProbes, persistence checks. CX Codex +1 411installs akka-net-management A skill that walks your agent through Akka.Management and Cluster Bootstrap: dynamic node discovery, health endpoints, Kubernetes and Azure setups. CX Codex +1 409installs opentelemetry-net-instrumentation A skill that guides OpenTelemetry instrumentation in .NET code: traces, metrics and logs. Useful when you add telemetry to a library or an app. CX Codex +1 408installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor