Communityコーディング&開発github.com

verify-email-snapshots

Verify を使用してメールテンプレートのスナップショットテストを行い、リグレッションを検出します。レンダリングされた HTML 出力が承認済みのベースラインと一致することを検証します。MJML テンプレートや任意のメールレンダラーで動作します。

verify-email-snapshots とは?

このスキルは、Verify ライブラリを使用してメールテンプレートのスナップショットテストを実施する方法を提供します。メールテンプレートは視覚的で複雑な HTML を生成するため、単体テストでは検出が難しいリグレッション(回帰バグ)が発生しやすいです。スナップショットテストにより、レンダリングされた HTML をキャプチャし、予期せぬ変更があった場合にテストを失敗させることで、問題を早期に発見できます。主に .NET 環境(XUnit、NUnit、MSTest)で動作し、MJML テンプレートのレンダリング、変数置換の検証、動的値のスクラビング(削除)に対応しています。また、テストフィクスチャのセットアップ、CI との統合、ビジュアル差分ツールやブラウザでのレビュー方法も網羅しており、チームでのコードレビューや継続的インテグレーションパイプラインに組み込みやすくなっています。このスキールドキュメントは、Verify を用いたスナップショットテストの導入からベストプラクティスまでを実践的に解説しています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/verify-email-snapshots

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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  # または 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. ビジュアル差分ツール

# 差分ツールの設定(一度だけ)
dotnet tool install -g verify.tool
verify accept  # 保留中の変更をすべて受け入れる
verify review  # 差分ツールを開く

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();  // URL 内の GUID を削除
}

よく使うスクラバー

// 日付をスクラブ
.ScrubLinesContaining("Date:")
.AddScrubber(s => Regex.Replace(s, @"\d{4}-\d{2}-\d{2}", "SCRUBBED-DATE"))

// トークンを含む URL をスクラブ
.AddScrubber(s => Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"))

// GUID をスクラブ
.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;
}

コンポーザーのスナップショットテスト

件名やメタデータを含む完全なコンポーザー出力をテストします:

[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 ファイルが存在しない場合に失敗させることで、誤った受け入れを防ぎます:

// テスト設定または ModuleInitializer で
VerifierSettings.ThrowOnMissingVerifiedFile();

Git 設定

差分の処理を改善するために .gitattributes に追加:

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

ベストプラクティス

推奨

// 推奨: 各テンプレートバリエーションをテストする
[Fact] Task WelcomeEmail_NewUser_RendersCorrectly()
[Fact] Task WelcomeEmail_InvitedUser_RendersCorrectly()

// 推奨: 説明的なテスト名を使用する
[Fact] Task PaymentReceipt_WithRefund_ShowsRefundAmount()

// 推奨: 動的な値を一貫してスクラブする
.ScrubLinesContaining("Generated at:")

// 推奨: 差分を慎重に確認してから受け入れる
verify review

非推奨

// 非推奨: メールテストをスキップする
// 非推奨: レビューなしで変更を自動受け入れる
verify accept --all  // 危険!

// 非推奨: ハッピーパスのみテストする
// 非推奨: スナップショットテストの失敗を無視する

ワークフロー

  1. テンプレートを作成 - MJML テンプレートを作成
  2. テストを書く - サンプル変数を使用してスナップショットテストを追加
  3. テストを実行 - 初回実行で .verified.html が作成される
  4. レビュー - ブラウザで開き、レンダリングを確認
  5. コミット - .verified.html をソース管理に含める
  6. 反復 - 変更があるとテストが失敗するため、差分を確認し、正しければ受け入れる

リソース

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

関連スキル