Community코딩 & 개발github.com

스냅샷 테스트 (Verify)

.NET에서 Verify를 사용한 스냅샷 테스트입니다. API 표면, HTTP 응답, 렌더링된 이메일, 직렬화된 출력을 승인하고, 사람이 검토한 베이스라인 파일을 통해 의도하지 않은 변경을 감지합니다.

스냅샷 테스트 (Verify)란 무엇인가요?

이 스킬은 .NET 애플리케이션에서 Verify 라이브러리를 사용한 스냅샷 테스트를 수행하는 방법을 설명합니다. 스냅샷 테스트는 출력 결과를 캡처하여 사람이 승인한 베이스라인 파일(.verified)과 비교함으로써 의도하지 않은 변경을 감지합니다. XUnit, NUnit, MSTest 등 주요 테스트 프레임워크를 지원하며, HTML 이메일 렌더링 검증, 공개 API 표면 승인, HTTP 응답 본문 및 헤더 검증, 복잡한 객체의 직렬화 출력 검증 등에 유용합니다. REFramework와 같은 CI/CD 환경에서도 사용할 수 있도록 설정 방법을 제공하며, 동적 값(타임스탬프, GUID)을 스크러빙하여 지속적인 테스트를 가능하게 합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/snapshot-testing

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Verify를 사용한 스냅샷 테스트

이 스킬을 사용해야 할 때

다음과 같은 경우 스냅샷 테스트를 사용하세요:

  • 렌더링된 출력 검증 (HTML 이메일, 보고서, 생성된 코드)
  • 변경 감지를 위한 공개 API 표면 승인
  • HTTP 응답 본문 및 헤더 테스트
  • 직렬화 출력 검증
  • 복잡한 개체의 의도하지 않은 변경 감지

스냅샷 테스트란?

스냅샷 테스트는 출력을 캡처하여 사람이 승인한 베이스라인과 비교합니다:

  1. 첫 번째 실행: 테스트가 실제 출력과 함께 .received. 파일을 생성합니다.
  2. 사람 검토: 개발자가 승인하여 .verified. 파일을 만듭니다.
  3. 이후 실행: 테스트가 출력을 .verified. 파일과 비교합니다.
  4. 변경 감지: 테스트가 실패하고 diff 도구가 차이점을 보여줍니다.

이 방법은 의도하지 않은 변경을 감지하면서 명시적인 승인을 통해 의도적인 변경을 허용합니다.


설치

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");

        // diff 도구 구성 (선택 사항 - 자동 감지)
        // 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" });

    // 올바른 파일 이름 지정을 위해 확장자 매개변수 사용
    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/레이아웃 회귀 감지
  • 깨진 템플릿 변수 탐지
  • diff 도구에서 시각적 검토
  • 버전 관리가 이메일 변경 사항 추적

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 diff에서 축소)
*.verified.txt linguist-generated=true
*.verified.html linguist-generated=true
*.verified.json linguist-generated=true

CI/CD 통합

확인된 파일 누락 시 실패 처리

[ModuleInitializer]
public static void Init()
{
    // CI에서는 diff 도구를 실행하는 대신 실패
    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 표면예우발적 호환성 변경 방지
직렬화 출력예와이어 형식 검증
복잡한 개체 그래프예수동 어설션보다 쉬움
간단한 값 확인아니요일반 어설션 사용
비즈니스 로직아니요명시적 어설션 사용
성능 테스트아니요벤치마크 사용

모범 사례

권장 사항

// 설명적인 테스트 이름 사용 - 파일 이름이 됨
[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. 승인하기 전에 diff 도구에서 변경 사항 검토

이 접근 방식으로 감지:

  • 깨진 변수 대체
  • CSS/레이아웃 회귀
  • 이메일 클라이언트 호환성 문제
  • 의도하지 않은 콘텐츠 변경

리소스

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

관련 스킬