mcpField
V
verify-email-snapshots

verify-email-snapshots

github.com/aarononthewebテスト・品質1.2k426 回インストール今日更新
リポジトリ

verify-email-snapshots について

Verify のスナップショットを使い、メール HTML の回帰検出やテンプレート変更のレビューを行う .NET チーム向けテストスキルです。

主な機能

  1. 01メール HTML のスナップショットテスト
  2. 02MJML のコンパイル結果確認
  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. 反復する - 変更によってテストが失敗したら、差分を確認し、正しければ承認します

リソース

コメント