Verify를 사용한 스냅샷 테스트
이 스킬을 사용해야 할 때
다음과 같은 경우 스냅샷 테스트를 사용하세요:
- 렌더링된 출력 검증 (HTML 이메일, 보고서, 생성된 코드)
- 변경 감지를 위한 공개 API 표면 승인
- HTTP 응답 본문 및 헤더 테스트
- 직렬화 출력 검증
- 복잡한 개체의 의도하지 않은 변경 감지
스냅샷 테스트란?
스냅샷 테스트는 출력을 캡처하여 사람이 승인한 베이스라인과 비교합니다:
- 첫 번째 실행: 테스트가 실제 출력과 함께
.received.파일을 생성합니다. - 사람 검토: 개발자가 승인하여
.verified.파일을 만듭니다. - 이후 실행: 테스트가 출력을
.verified.파일과 비교합니다. - 변경 감지: 테스트가 실패하고 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 스킬을 참조하세요:
{{variable}}플레이스홀더가 포함된 MJML 템플릿- 테스트 데이터로 HTML 렌더링
- 렌더링된 출력을 스냅샷 테스트
- 승인하기 전에 diff 도구에서 변경 사항 검토
이 접근 방식으로 감지:
- 깨진 변수 대체
- 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