verify-email-snapshots について
Verify のスナップショットを使い、メール HTML の回帰検出やテンプレート変更のレビューを行う .NET チーム向けテストスキルです。
主な機能
- 01メール HTML のスナップショットテスト
- 02MJML のコンパイル結果確認
- 03テンプレート変更差分のレビュー
- 04動的な値のスクラブ
- 05メール合成結果のテスト
活用シーン
- 01メール描画の意図しない変更の検出
- 02テンプレート変数置換の確認
- 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 の一般的なパターン
メールをスナップショットテストする理由
メールテンプレートには次の特徴があります。
- 視覚的な要素です - 小さな変更でも、各クライアントでのレンダリングが崩れることがあります
- ユニットテストが困難です - 出力は単純な値ではなく、複雑な HTML です
- リグレッションが発生しやすいです - テンプレートの変更が意図しない影響を及ぼすことがあります
スナップショットテストではレンダリングされた 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
ワークフロー
- テンプレートを作成する - MJML テンプレートを作成します
- テストを作成する - サンプル変数を使ったスナップショットテストを追加します
- テストを実行する - 初回実行時に
.verified.htmlが作成されます - 確認する - ブラウザーで開き、レンダリングを検証します
- コミットする -
.verified.htmlをソース管理に含めます - 反復する - 変更によってテストが失敗したら、差分を確認し、正しければ承認します
リソース
- Verify: https://github.com/VerifyTests/Verify
- Verify.Xunit: https://github.com/VerifyTests/Verify#xunit
- 差分ツール: https://github.com/VerifyTests/DiffEngine
コメント
Markdown 対応