Community코딩 & 개발github.com

이메일-템플릿-스냅샷-테스트

Verify를 사용하여 이메일 템플릿의 스냅샷 테스트를 수행하고, 렌더링된 HTML이 승인된 기준과 일치하는지 검증하여 회귀 오류를 잡아냅니다. MJML 템플릿 및 모든 이메일 렌더러와 함께 작동합니다.

이메일-템플릿-스냅샷-테스트란 무엇인가요?

이 스킬은 Verify 스냅샷 테스트 라이브러리를 활용하여 이메일 템플릿의 시각적 회귀 오류를 감지하는 방법을 제공합니다. 이메일 템플릿은 복잡한 HTML을 출력하므로 단위 테스트로 검증하기 어렵고, 변경 사항이 클라이언트 간 렌더링에 영향을 줄 수 있습니다. 스냅샷 테스트는 렌더링된 HTML을 캡처하고 예상치 못한 변경이 발생하면 테스트를 실패시켜 개발자에게 변경 사항을 신속히 알립니다. Verify.Xunit, Verify.NUnit, Verify.MSTest 등 다양한 테스트 프레임워크를 지원하며, MJML 템플릿 렌더러와 같은 모든 이메일 렌더러와 호환됩니다. 주요 기능으로는 템플릿 변형별 테스트, 동적 값 스크러빙(날짜, GUID 등), CI 통합, 그리고 Git에서 `.verified.html` 파일을 추적하여 변경 이력을 관리할 수 있습니다. 이 스킬은 코드 리뷰 시 시각적 diff를 제공하여 템플릿 변경의 영향을 명확히 파악하고, 누락된 기준 파일이 있을 경우 CI에서 자동 실패하도록 설정할 수 있어 엄격한 품질 관리를 가능하게 합니다.

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

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

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

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

문서

Verify를 사용한 이메일 템플릿 스냅샷 테스트

이 스킬을 사용해야 할 때

다음과 같은 경우 이 스킬을 사용하세요:

  • 이메일 템플릿 렌더링에서 회귀 오류를 테스트할 때
  • MJML 템플릿이 예상된 HTML로 컴파일되는지 검증할 때
  • 코드 리뷰에서 이메일 변경 사항을 검토할 때 (diff가 시각적)
  • 변수 치환이 올바르게 작동하는지 확인할 때

관련 스킬:

  • aspnetcore/mjml-email-templates - MJML 템플릿 작성
  • aspire/mailpit-integration - 로컬에서 이메일 전달 테스트
  • testing/snapshot-testing - 일반적인 Verify 패턴

이메일을 스냅샷 테스트해야 하는 이유

이메일 템플릿은 다음과 같은 특징이 있습니다:

  1. 시각적 - 작은 변경이 클라이언트 간 렌더링을 망가뜨릴 수 있음
  2. 단위 테스트가 어려움 - 출력이 단순한 값이 아닌 복잡한 HTML
  3. 회귀 오류에 취약함 - 템플릿 변경이 의도치 않은 영향을 줄 수 있음

스냅샷 테스트는 렌더링된 HTML을 캡처하고 예상치 못한 변경이 발생하면 실패합니다.


설치

dotnet add package Verify.Xunit  # 또는 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 파일이 생성됩니다.


이메일 변경 사항 검토

템플릿이 변경되면 테스트가 실패하고 diff가 표시됩니다. 검토 옵션:

1. 시각적 Diff 도구

# Diff 도구 설정 (한 번만 실행)
dotnet tool install -g verify.tool
verify accept  # 모든 보류 중인 변경 사항 수락
verify review  # Diff 도구 열기

2. 브라우저 미리보기

.received.html 파일을 브라우저에서 열어 실제 렌더링을 확인합니다.

3. IDE 통합

대부분의 IDE에서 .verified.html과 .received.html 파일의 인라인 diff를 보여줍니다.


각 템플릿 변형 테스트

회귀 오류를 발견하기 위해 각 이메일 템플릿에 대한 테스트를 만드세요:

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();  // URL 내 GUID 스크러빙
}

일반적인 스크러버

// 날짜 스크러빙
.ScrubLinesContaining("Date:")
.AddScrubber(s => Regex.Replace(s, @"\d{4}-\d{2}-\d{2}", "SCRUBBED-DATE"))

// 토큰이 포함된 URL 스크러빙
.AddScrubber(s => Regex.Replace(s, @"token=[a-zA-Z0-9]+", "token=SCRUBBED"))

// GUID 스크러빙
.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;
}

컴포저 스냅샷 테스트

제목과 메타데이터를 포함한 전체 컴포저 출력을 테스트하세요:

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

    // 전체 이메일 객체 검증 (제목, 수신자, 본문)
    await Verify(new
    {
        email.To,
        email.Subject,
        HtmlBody = email.HtmlBody  // .html 확장자로 저장됨
    });
}

CI 통합

기준 파일 누락 시 실패

CI에서 .verified.html 파일이 없으면 실패하도록 설정하여 우발적인 수락을 방지합니다:

// 테스트 설정 또는 ModuleInitializer에서
VerifierSettings.ThrowOnMissingVerifiedFile();

Git 구성

.gitattributes에 추가하여 diff 처리를 개선합니다:

*.verified.html linguist-language=HTML
*.verified.html diff=html

모범 사례

해야 할 일

// DO: 각 템플릿 변형을 테스트
[Fact] Task WelcomeEmail_NewUser_RendersCorrectly()
[Fact] Task WelcomeEmail_InvitedUser_RendersCorrectly()

// DO: 설명적인 테스트 이름 사용
[Fact] Task PaymentReceipt_WithRefund_ShowsRefundAmount()

// DO: 동적 값을 일관되게 스크러빙
.ScrubLinesContaining("Generated at:")

// DO: 수락 전에 diff를 신중히 검토
verify review

하지 말아야 할 일

// DON'T: 이메일 테스트를 건너뛰기
// DON'T: 검토 없이 자동으로 변경 사항을 수락
verify accept --all  // 위험!

// DON'T: 행복 경로만 테스트
// DON'T: 스냅샷 테스트 실패를 무시

워크플로우

  1. 템플릿 생성 - MJML 템플릿을 작성합니다
  2. 테스트 작성 - 샘플 변수로 스냅샷 테스트를 추가합니다
  3. 테스트 실행 - 첫 실행 시 .verified.html이 생성됩니다
  4. 검토 - 브라우저에서 열어 렌더링을 확인합니다
  5. 커밋 - .verified.html 파일을 소스 제어에 포함합니다
  6. 반복 - 변경 사항이 테스트를 실패시키고, diff를 검토한 후 올바르면 수락합니다

리소스

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.

관련 스킬