Verifyを使ったスナップショットテスト
このスキルを使用するタイミング
以下の場合にスナップショットテストを使用します:
- レンダリングされた出力(HTMLメール、レポート、生成コード)の検証
- 破壊的変更検出のための公開APIサーフェスの承認
- HTTPレスポンス本文とヘッダーのテスト
- シリアライズ出力の検証
- 複雑なオブジェクトの意図しない変更のキャッチ
スナップショットテストとは
スナップショットテストは出力をキャプチャし、人間が承認したベースラインと比較します:
- 初回実行:テストが
.received.ファイルに実際の出力を生成 - 人間によるレビュー:開発者が承認し、
.verified.ファイルを作成 - 以降の実行:テストが出力を
.verified.ファイルと比較 - 変更の検出:テストが失敗し、差分ツールが差異を表示
これにより、意図しない変更をキャッチしつつ、意図的な変更は明示的な承認を通じて許可します。
インストール
Verifyパッケージの追加
dotnet add package Verify.Xunit
# または他のテストフレームワーク用:
dotnet add package Verify.NUnit
dotnet add package Verify.MSTest
ModuleInitializerの設定
テストプロジェクトにModuleInitializer.csを作成:
using System.Runtime.CompilerServices;
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Init()
{
// 検証済みファイルにソースファイル相対パスを使用
VerifyBase.UseProjectRelativeDirectory("Snapshots");
// 差分ツールの設定(オプション - 自動検出)
// DiffTools.UseOrder(DiffTool.Rider, DiffTool.VisualStudioCode);
}
}
基本的な使い方
単純なオブジェクトの検証
[Fact]
public Task VerifyUserDto()
{
var user = new UserDto(
Id: "user-123",
Name: "John Doe",
Email: "[email protected]",
CreatedAt: new DateTime(2025, 1, 15));
return Verify(user);
}
VerifyUserDto.verified.txtを作成:
{
Id: user-123,
Name: John Doe,
Email: [email protected],
CreatedAt: 2025-01-15T00:00:00
}
文字列/HTMLの検証
[Fact]
public async Task VerifyRenderedEmail()
{
var html = await _emailRenderer.RenderAsync("Welcome", new { Name = "John" });
// 適切なファイル名のためにextensionパラメーターを使用
await Verify(html, extension: "html");
}
VerifyRenderedEmail.verified.htmlを作成 - ブラウザで表示可能。
メールテンプレートのテスト
Verifyを使用して、レンダリングされたメールテンプレートの意図しない変更をキャッチします:
[Fact]
public async Task UserSignupInvitation_RendersCorrectly()
{
var renderer = _services.GetRequiredService<IMjmlTemplateRenderer>();
var variables = new Dictionary<string, string>
{
{ "OrganizationName", "Acme Corporation" },
{ "InviteeName", "John Doe" },
{ "InviterName", "Jane Admin" },
{ "InvitationLink", "https://example.com/invite/abc123" },
{ "ExpirationDate", "December 31, 2025" }
};
var html = await renderer.RenderTemplateAsync(
"UserInvitations/UserSignupInvitation",
variables);
await Verify(html, extension: "html");
}
メールテストの利点:
- CSS/レイアウトのリグレッションをキャッチ
- 壊れたテンプレート変数を検出
- 差分ツールでのビジュアルレビュー
- バージョン管理でメールの変更を追跡
APIサーフェスの承認
公開APIへの偶発的な破壊的変更を防止:
[Fact]
public Task ApprovePublicApi()
{
var assembly = typeof(MyLibrary.PublicClass).Assembly;
var publicApi = assembly.GetExportedTypes()
.OrderBy(t => t.FullName)
.Select(t => new
{
Type = t.FullName,
Members = t.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(m => m.DeclaringType == t)
.OrderBy(m => m.Name)
.Select(m => m.ToString())
});
return Verify(publicApi);
}
または、専用のApiApproverパッケージを使用:
dotnet add package PublicApiGenerator
dotnet add package Verify.Xunit
[Fact]
public Task ApproveApi()
{
var api = typeof(MyPublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}
完全なAPIサーフェスを含む.verified.txtを作成 - 変更には明示的な承認が必要です。
HTTPレスポンスのテスト
[Fact]
public async Task GetUser_ReturnsExpectedResponse()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/users/123");
// ステータス、ヘッダー、本文を同時に検証
await Verify(new
{
StatusCode = response.StatusCode,
Headers = response.Headers
.Where(h => h.Key.StartsWith("X-")) // カスタムヘッダーのみ
.ToDictionary(h => h.Key, h => h.Value.First()),
Body = await response.Content.ReadAsStringAsync()
});
}
動的な値のスクラビング
タイムスタンプ、GUID、その他の動的なコンテンツを処理:
[Fact]
public Task VerifyOrder()
{
var order = new Order
{
Id = Guid.NewGuid(), // 実行ごとに異なる
CreatedAt = DateTime.UtcNow, // 実行ごとに異なる
Total = 99.99m
};
return Verify(order)
.ScrubMember("Id") // プレースホルダーに置き換え
.ScrubMember("CreatedAt");
}
出力:
{
Id: Guid_1,
CreatedAt: DateTime_1,
Total: 99.99
}
グローバルスクラビング
ModuleInitializerで設定:
[ModuleInitializer]
public static void Init()
{
VerifierSettings.ScrubMembersWithType<DateTime>();
VerifierSettings.ScrubMembersWithType<DateTimeOffset>();
VerifierSettings.ScrubMembersWithType<Guid>();
// 特定のパターンをスクラブ
VerifierSettings.AddScrubber(s =>
Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"));
}
ファイル構成
推奨構成
tests/
MyApp.Tests/
Snapshots/ # すべての検証済みファイル
EmailTests/
WelcomeEmail.verified.html
PasswordReset.verified.html
ApiTests/
GetUser.verified.txt
EmailTests.cs
ApiTests.cs
ModuleInitializer.cs
.gitignore
# Verify - receivedファイルは無視(verifiedのみをコミット)
*.received.*
.gitattributes
# 検証済みファイルを生成物として扱う(PR差分で折りたたむ)
*.verified.txt linguist-generated=true
*.verified.html linguist-generated=true
*.verified.json linguist-generated=true
CI/CD統合
検証済みファイルがない場合に失敗
[ModuleInitializer]
public static void Init()
{
// CIでは、差分ツールを起動せずに失敗させる
if (Environment.GetEnvironmentVariable("CI") == "true")
{
VerifyDiffPlex.UseDiffPlex(OutputType.Minimal);
DiffRunner.Disabled = true;
}
}
GitHub Actions
- name: テストを実行
run: dotnet test
env:
CI: true
- name: 失敗時にスナップショットをアップロード
if: failure()
uses: actions/upload-artifact@v4
with:
name: snapshots
path: |
**/*.received.*
**/*.verified.*
スナップショットテストを使用するタイミング
| シナリオ | スナップショットテストを使用するか? | 理由 |
|---|---|---|
| レンダリングされたHTML/メール | はい | ビジュアルリグレッションをキャッチ |
| APIサーフェス | はい | 偶発的な破壊を防止 |
| シリアライズ出力 | はい | ワイヤーフォーマットを検証 |
| 複雑なオブジェクトグラフ | はい | 手動アサーションより簡単 |
| 単純な値のチェック | いいえ | 通常のアサーションを使用 |
| ビジネスロジック | いいえ | 明示的なアサーションを使用 |
| パフォーマンステスト | いいえ | ベンチマークを使用 |
ベストプラクティス
推奨事項
// テスト名に説明を記載 - それがファイル名になる
[Fact]
public Task UserRegistration_WithValidData_ReturnsConfirmation()
// 動的な値を一貫してスクラブ
VerifierSettings.ScrubMembersWithType<Guid>();
// テキスト以外のコンテンツにはextensionパラメーターを使用
await Verify(html, extension: "html");
// 検証済みファイルをソース管理に保持
git add *.verified.*
禁止事項
// スクラブなしでランダム/動的データを検証しない
var order = new Order { Id = Guid.NewGuid() }; // 毎回失敗!
await Verify(order);
// .receivedファイルをコミットしない
git add *.received.* // 間違い!
// 単純なアサーションには使用しない
await Verify(result.Count); // 単に Assert.Equal(5, result.Count) を使用すべき
MJMLメールテストとの統合
完全なパターンについては、aspnetcore/transactional-emailsスキルを参照:
{{variable}}プレースホルダー付きのMJMLテンプレート- テストデータでHTMLにレンダリング
- レンダリングされた出力をスナップショットテスト
- 承認前に差分ツールで変更をレビュー
これにより以下をキャッチ:
- 壊れた変数置換
- CSS/レイアウトのリグレッション
- メールクライアントの互換性問題
- 意図しないコンテンツ変更
参考資料
- Verify GitHub: https://github.com/VerifyTests/Verify
- Verify.Xunit: https://github.com/VerifyTests/Verify.Xunit
- ApiApprover: https://github.com/JakeGinnivan/ApiApprover
- DiffPlex Integration: https://github.com/VerifyTests/Verify.DiffPlex