mcpField
V
verify-email-snapshots

verify-email-snapshots

github.com/aaronontheweb测试与质量1.2k426 次安装今天更新
代码仓库

关于 verify-email-snapshots

面向 .NET 团队的邮件测试技能,使用 Verify 快照捕捉渲染后 HTML 的回归,并审查模板改动。

主要功能

  1. 01为渲染后的邮件 HTML 编写快照测试
  2. 02检查 MJML 编译输出
  3. 03审查模板变更差异
  4. 04清理动态值
  5. 05测试邮件组合结果

适用场景

  1. 01发现邮件渲染的意外变化
  2. 02检查模板变量替换是否正确
  3. 03在代码审查中查看 HTML 变更

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill verify-email-snapshots

安装后在 Claude Code 中输入/verify-email-snapshots确认已加载。

详细介绍

使用 Verify 对电子邮件模板进行快照测试

何时使用此技能

在以下情况下使用此技能:

  • 测试电子邮件模板渲染是否出现回归
  • 验证 MJML 模板是否编译为预期的 HTML
  • 在代码审查中检查电子邮件变更(差异以视觉形式呈现)
  • 确保变量替换正常工作

相关技能:

  • aspnetcore/mjml-email-templates - 编写 MJML 模板
  • aspire/mailpit-integration - 在本地测试电子邮件投递
  • testing/snapshot-testing - Verify 的通用用法模式

为什么要对电子邮件进行快照测试?

电子邮件模板具有以下特点:

  1. 注重视觉效果 - 细微改动就可能导致不同客户端中的渲染出现问题
  2. 难以进行单元测试 - 输出是复杂的 HTML,而不是简单的值
  3. 容易出现回归 - 模板变更可能产生意料之外的影响

快照测试会捕获渲染后的 HTML,并在其发生意外变化时失败。


安装

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

基本的电子邮件快照测试

[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");
}

首次运行时,这会创建 UserSignupInvitation_RendersCorrectly.verified.html。


审查电子邮件变更

模板发生变更时,测试会失败并显示差异。可通过以下方式审查:

1. 可视化差异工具
# Configure diff tool (one-time)
dotnet tool install -g verify.tool
verify accept  # Accept all pending changes
verify review  # Open diff tool
2. 浏览器预览

在浏览器中打开 .received.html 文件,查看实际渲染效果。

3. IDE 集成

大多数 IDE 都会以内联差异的形式显示 .verified.html 与 .received.html 文件之间的区别。


测试每个模板变体

为每个电子邮件模板创建测试,以发现回归:

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("/", "_"));
    }
}

清理动态值

某些值会在不同测试运行之间变化。清理这些值:

[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
}
常用清理器
// 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()

电子邮件测试的测试夹具

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 快照测试

测试完整的 composer 输出,包括主题和元数据:

[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 集成

缺少基线时失败

在 CI 中,如果不存在 .verified.html 文件则令测试失败(防止意外接受变更):

// In test setup or ModuleInitializer
VerifierSettings.ThrowOnMissingVerifiedFile();
Git 配置

在 .gitattributes 中添加以下配置,以改善差异处理:

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

最佳实践

应该做
// 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: 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

工作流

  1. 创建模板 - 编写 MJML 模板
  2. 编写测试 - 使用示例变量添加快照测试
  3. 运行测试 - 首次运行会创建 .verified.html
  4. 审查 - 在浏览器中打开并验证渲染效果
  5. 提交 - 将 .verified.html 纳入源代码管理
  6. 迭代 - 变更会导致测试失败;审查差异,并在确认正确后接受

资源

评论

支持 Markdown