快照测试 snapshot-testing

快照测试技能用于在.NET开发中自动捕获和比较软件输出,通过基线文件检测意外更改。适用于API接口验证、HTTP响应测试、电子邮件模板渲染、序列化输出检查等场景。关键词:快照测试、Verify.NET、API测试、回归测试、自动化测试、.NET开发、软件质量、测试框架、CI/CD集成、测试自动化。

测试 0 次安装 0 次浏览 更新于 2/26/2026

name: 快照测试 description: 在 .NET 中使用 Verify 进行快照测试。批准 API 接口、HTTP 响应、渲染的电子邮件和序列化输出。通过人工审查的基线文件检测意外更改。 invocable: false

使用 Verify 进行快照测试

何时使用此技能

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

  • 验证渲染输出(HTML 电子邮件、报告、生成的代码)
  • 批准公共 API 接口以检测破坏性更改
  • 测试 HTTP 响应体和头部
  • 验证序列化输出
  • 捕获复杂对象中的意外更改

什么是快照测试?

快照测试捕获输出并将其与人工批准的基线进行比较:

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

这可以捕获意外更改,同时通过显式批准允许有意更改


安装

添加 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: "john@example.com",
        CreatedAt: new DateTime(2025, 1, 15));

    return Verify(user);
}

创建 VerifyUserDto.verified.txt

{
  Id: user-123,
  Name: John Doe,
  Email: john@example.com,
  CreatedAt: 2025-01-15T00:00:00
}

字符串/HTML 验证

[Fact]
public async Task VerifyRenderedEmail()
{
    var html = await _emailRenderer.RenderAsync("Welcome", new { Name = "John" });

    // 使用扩展参数进行正确的文件命名
    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.*

.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>();

// 对非文本内容使用扩展参数
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 技能:

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

这可以捕获:

  • 损坏的变量替换
  • CSS/布局回归
  • 电子邮件客户端兼容性问题
  • 意外的内容更改

资源