mcpField
S
snapshot-testing

snapshot-testing

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

关于 snapshot-testing

snapshot-testing 面向使用 Verify 的 .NET 开发者,通过人工批准的基线比较测试输出。内容涵盖渲染内容、公共 API、HTTP 响应和序列化对象。

主要功能

  1. 01经批准的输出基线
  2. 02HTML 与对象快照
  3. 03公共 API 变更检测
  4. 04动态值清理
  5. 05CI 快照检查

适用场景

  1. 01审查渲染邮件的变化
  2. 02检测意外的公共 API 破坏性变更
  3. 03检查 HTTP 响应与序列化结果

安装

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

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill snapshot-testing

安装后在 Claude Code 中输入/snapshot-testing确认已加载。

详细介绍

使用 Verify 进行快照测试

何时使用此技能

在以下情况下使用快照测试:

  • 验证渲染输出(HTML 邮件、报告、生成的代码)
  • 审核公共 API 接口,以检测破坏性变更
  • 测试 HTTP 响应正文和标头
  • 验证序列化输出
  • 发现复杂对象中的意外变更

什么是快照测试?

快照测试会捕获输出,并与经过人工审核的基准进行比较:

  1. 首次运行:测试生成包含实际输出的 .received. 文件
  2. 人工审核:开发者批准该文件,从而创建 .verified. 文件
  3. 后续运行:测试将输出与 .verified. 文件进行比较
  4. 检测到变更:测试失败,差异工具会显示差异供审核

这能发现意外变更,同时允许通过明确批准来接受有意变更。


安装

添加 Verify 包
dotnet add package Verify.Xunit
# or for other test frameworks:
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()
    {
        // Use source-file-relative paths for verified files
        VerifyBase.UseProjectRelativeDirectory("Snapshots");

        // Configure diff tool (optional - auto-detected)
        // 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" });

    // Use extension parameter for proper file naming
    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");

    // Verify status, headers, and body together
    await Verify(new
    {
        StatusCode = response.StatusCode,
        Headers = response.Headers
            .Where(h => h.Key.StartsWith("X-"))  // Custom headers only
            .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(),  // Different every run
        CreatedAt = DateTime.UtcNow,  // Different every run
        Total = 99.99m
    };

    return Verify(order)
        .ScrubMember("Id")  // Replace with placeholder
        .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>();

    // Scrub specific patterns
    VerifierSettings.AddScrubber(s =>
        Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"));
}

文件组织

推荐结构
tests/
  MyApp.Tests/
    Snapshots/           # All verified files
      EmailTests/
        WelcomeEmail.verified.html
        PasswordReset.verified.html
      ApiTests/
        GetUser.verified.txt
    EmailTests.cs
    ApiTests.cs
    ModuleInitializer.cs
.gitignore
# Verify - ignore received files (only commit verified)
*.received.*
.gitattributes
# Treat verified files as generated (collapse in PR diffs)
*.verified.txt linguist-generated=true
*.verified.html linguist-generated=true
*.verified.json linguist-generated=true

CI/CD 集成

缺少已验证文件时失败
[ModuleInitializer]
public static void Init()
{
    // In CI, fail instead of launching diff tool
    if (Environment.GetEnvironmentVariable("CI") == "true")
    {
        VerifyDiffPlex.UseDiffPlex(OutputType.Minimal);
        DiffRunner.Disabled = true;
    }
}
GitHub Actions
- name: Run tests
  run: dotnet test
  env:
    CI: true

- name: Upload snapshots on failure
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: snapshots
    path: |
      **/*.received.*
      **/*.verified.*

何时使用快照测试

场景使用快照测试?原因
渲染后的 HTML/邮件是发现视觉回归
API 接口是防止意外破坏
序列化输出是验证传输格式
复杂对象图是比手动断言更简单
简单值检查否使用常规断言
业务逻辑否使用明确的断言
性能测试否使用基准测试

最佳实践

应该做
// Use descriptive test names - they become file names
[Fact]
public Task UserRegistration_WithValidData_ReturnsConfirmation()

// Scrub dynamic values consistently
VerifierSettings.ScrubMembersWithType<Guid>();

// Use extension parameter for non-text content
await Verify(html, extension: "html");

// Keep verified files in source control
git add *.verified.*
不应该做
// Don't verify random/dynamic data without scrubbing
var order = new Order { Id = Guid.NewGuid() };  // Fails every run!
await Verify(order);

// Don't commit .received files
git add *.received.*  // Wrong!

// Don't use for simple assertions
await Verify(result.Count);  // Just use Assert.Equal(5, result.Count)

与 MJML 邮件测试集成

有关完整模式,请参阅 mjml-email-templates 和 verify-email-snapshots 技能:

  1. 使用 {{variable}} 占位符的 MJML 模板
  2. 使用测试数据渲染为 HTML
  3. 对渲染后的输出进行快照测试
  4. 批准前在差异工具中审核变更

这有助于发现:

  • 变量替换失效
  • CSS/布局回归
  • 邮件客户端兼容性问题
  • 意外的内容变更

资源

评论

支持 Markdown