Verify Snapshot Test
소스 생성기가 만들어내는 코드는 수십~수백 줄에 달합니다. 이를 assertion 하나하나로 검증하면 테스트 코드가 생성 코드보다 더 복잡해집니다. 스냅샷 테스트는 생성된 코드 전체를 .verified.txt 파일로 저장하고, 이후 실행 결과와 자동으로 비교하는 방식입니다. 변경이 생기면 diff를 보여주므로, 의도한 변경인지 실수인지 즉시 판별할 수 있습니다. 소스 생성기처럼 출력이 결정적(deterministic)이고 전체 형태가 중요한 경우에 특히 적합한 테스트 전략입니다.
학습 목표
섹션 제목: “학습 목표”핵심 학습 목표
섹션 제목: “핵심 학습 목표”- Verify 라이브러리 이해
- xUnit과 통합되는 스냅샷 테스트 프레임워크의 동작 방식
- .verified.txt 파일 관리
- 승인(approve) 워크플로우와 소스 제어 전략
- 스냅샷 테스트 워크플로우
- 첫 실행부터 변경 감지, 승인까지의 전체 과정
스냅샷 테스트란?
섹션 제목: “스냅샷 테스트란?”스냅샷 테스트는 출력 결과를 파일로 저장하고 이후 테스트에서 비교하는 방식입니다.
첫 번째 실행============입력 → 생성 → 결과 저장 (*.verified.txt)
이후 실행=========입력 → 생성 → 저장된 결과와 비교 │ ├─ 일치 → 테스트 통과 │ └─ 불일치 → 테스트 실패 │ └─ *.received.txt 생성 (비교용)Verify 라이브러리
섹션 제목: “Verify 라이브러리”<PackageReference Include="Verify.Xunit" Version="28.9.2" />기본 사용법
섹션 제목: “기본 사용법”using VerifyXunit;
[UsesVerify]public class MyTests{ [Fact] public Task Should_Generate_Expected_Output() { string result = GenerateSomething();
return Verify(result); }}소스 생성기 테스트에서 사용
섹션 제목: “소스 생성기 테스트에서 사용”[Fact]public Task Should_Generate_ObservableClass(){ string input = """ [GenerateObservablePort] public class TestAdapter : IObservablePort { public virtual FinT<IO, int> GetValue() => FinT<IO, int>.Succ(42); } """;
string? actual = _sut.Generate(input);
// 생성된 코드를 스냅샷과 비교 return Verify(actual);}.verified.txt 파일
섹션 제목: “.verified.txt 파일”파일 명명 규칙
섹션 제목: “파일 명명 규칙”{TestClassName}.{TestMethodName}.verified.txt
예시:ObservablePortGeneratorTests.ObservablePortGenerator_ShouldGenerate_ObservableClass_WithSingleMethod.verified.txt파일 내용
섹션 제목: “파일 내용”// ObservablePortGeneratorTests.*.verified.txt//------------------------------------------------------------------------------// <auto-generated>// This code was generated by source generator//// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.// </auto-generated>//------------------------------------------------------------------------------
#nullable enable
using System.Diagnostics;using System.Diagnostics.Metrics;using Functorium.Adapters.Observabilities;using Functorium.Adapters.Observabilities.Naming;using Functorium.Abstractions.Observabilities;
using LanguageExt;using Microsoft.Extensions.Logging;using Microsoft.Extensions.Options;
namespace TestNamespace;
public class TestAdapterObservable : TestAdapter{ private readonly ActivitySource _activitySource; private readonly ILogger<TestAdapterObservable> _logger; // ... 생성된 전체 코드}테스트 워크플로우
섹션 제목: “테스트 워크플로우”1. 새 테스트 작성
섹션 제목: “1. 새 테스트 작성”[Fact]public Task Should_Generate_NewFeature(){ string input = """ // 새로운 시나리오 입력 """;
string? actual = _sut.Generate(input);
return Verify(actual);}2. 첫 실행 - 실패
섹션 제목: “2. 첫 실행 - 실패”[xUnit.net 00:00:01.23] Should_Generate_NewFeature [FAIL]
Verify returned: - Received: ...NewFeature.received.txt - Verified: ...NewFeature.verified.txt (not found)3. received.txt 검토
섹션 제목: “3. received.txt 검토”# 생성된 파일 확인cat Tests/.../Should_Generate_NewFeature.received.txt4. 승인 (Approve)
섹션 제목: “4. 승인 (Approve)”# received.txt를 verified.txt로 이름 변경mv *.received.txt *.verified.txt
# 또는 도구 사용dotnet tool run verify.tool accept5. 이후 실행 - 통과
섹션 제목: “5. 이후 실행 - 통과”[xUnit.net 00:00:01.23] Should_Generate_NewFeature [PASS]변경 감지
섹션 제목: “변경 감지”의도적 변경
섹션 제목: “의도적 변경”소스 생성기를 수정하면 출력이 변경됩니다.
// 변경 전sb.AppendLine(" private readonly ActivitySource _activitySource;");
// 변경 후sb.AppendLine(" private readonly ActivitySource _newActivitySource;");테스트 결과
섹션 제목: “테스트 결과”[xUnit.net 00:00:01.23] Should_Generate_ObservableClass [FAIL]
Verify mismatch: - Received: _activityContext - Verified: _parentContextDiff 도구로 비교
섹션 제목: “Diff 도구로 비교”# diff 도구 사용diff *.verified.txt *.received.txt
# Visual Studio Codecode --diff *.verified.txt *.received.txt변경 승인
섹션 제목: “변경 승인”# 의도적 변경이면 새 버전 승인mv *.received.txt *.verified.txt소스 제어 통합
섹션 제목: “소스 제어 통합”.verified.txt 커밋
섹션 제목: “.verified.txt 커밋”# verified.txt 파일은 반드시 커밋git add *.verified.txtgit commit -m "test: update verified snapshots".received.txt 무시
섹션 제목: “.received.txt 무시”*.received.txt*.received/CI에서 검증
섹션 제목: “CI에서 검증”# CI 파이프라인steps: - run: dotnet test # 스냅샷 불일치 시 CI 실패실제 테스트 예시
섹션 제목: “실제 테스트 예시”단일 메서드 테스트
섹션 제목: “단일 메서드 테스트”[Fact]public Task ObservablePortGenerator_ShouldGenerate_ObservableClass_WithSingleMethod(){ string input = """ using Functorium.Adapters.SourceGenerators; using Functorium.Abstractions.Observabilities; using LanguageExt;
namespace TestNamespace;
public interface ITestAdapter : IObservablePort { FinT<IO, int> GetValue(); }
[GenerateObservablePort] public class TestAdapter : ITestAdapter { public string RequestCategory => "Test"; public virtual FinT<IO, int> GetValue() => FinT<IO, int>.Succ(42); } """;
string? actual = _sut.Generate(input);
return Verify(actual);}해당 .verified.txt 파일
섹션 제목: “해당 .verified.txt 파일”// 파일명: TestAdapter.Observable.g.cs
public class TestAdapterObservable : TestAdapter{ private readonly ActivitySource _activitySource; private readonly ILogger<TestAdapterObservable> _logger; private readonly Counter<long> _requestCounter; private readonly Counter<long> _responseCounter; private readonly Histogram<double> _durationHistogram;
// ... 전체 생성된 코드}테스트 파일 구조
섹션 제목: “테스트 파일 구조”Tests/Functorium.Tests.Unit/└── AdaptersTests/ └── SourceGenerators/ ├── ObservablePortGeneratorTests.cs │ │ // .verified.txt 파일들 (31개) ├── ObservablePortGeneratorTests.*.WithSingleMethod.verified.txt ├── ObservablePortGeneratorTests.*.WithMultipleMethods.verified.txt ├── ObservablePortGeneratorTests.*.WithPrimaryConstructor.verified.txt
└── ...장점과 주의점
섹션 제목: “장점과 주의점”| 장점 | 설명 |
|---|---|
| 전체 출력 검증 | 생성된 코드 전체를 검증 |
| 변경 추적 | Git으로 변경 이력 관리 |
| 리팩토링 안전성 | 의도치 않은 변경 즉시 감지 |
| 문서화 | verified.txt가 예상 출력 문서 역할 |
주의점
섹션 제목: “주의점”| 주의점 | 해결 방법 |
|---|---|
| 파일 수 증가 | 테스트별 하나씩 필요 |
| 비결정적 출력 | 결정적 출력 보장 필수 |
| 리뷰 복잡도 | PR에서 변경된 파일 검토 필요 |
베스트 프랙티스
섹션 제목: “베스트 프랙티스”1. 테스트 이름 명확하게
섹션 제목: “1. 테스트 이름 명확하게”// ✅ 좋은 예public Task Should_Generate_ObservableClass_WithPrimaryConstructor()
// ❌ 나쁜 예public Task Test1()2. 하나의 시나리오만 테스트
섹션 제목: “2. 하나의 시나리오만 테스트”// ✅ 좋은 예 - 한 가지만 테스트[Fact]public Task Should_Generate_Count_ForCollectionParameter() { }
[Fact]public Task Should_Generate_Length_ForArrayParameter() { }
// ❌ 나쁜 예 - 여러 가지 혼합[Fact]public Task Should_Generate_CountAndLength() { }3. 입력 최소화
섹션 제목: “3. 입력 최소화”// ✅ 좋은 예 - 테스트에 필요한 최소 코드string input = """ [GenerateObservablePort] public class TestAdapter : IObservablePort { public virtual FinT<IO, List<int>> GetItems() => ...; } """;
// ❌ 나쁜 예 - 불필요한 코드 포함string input = """ // 불필요한 주석 public interface IOtherInterface { } // 테스트와 무관
[GenerateObservablePort] public class TestAdapter : IObservablePort { private readonly string _unused; // 테스트와 무관 public virtual FinT<IO, List<int>> GetItems() => ...; } """;한눈에 보는 정리
섹션 제목: “한눈에 보는 정리”Verify 스냅샷 테스트의 워크플로우는 “실행 → 비교 → 승인”의 세 단계입니다. .verified.txt는 승인된 예상 출력으로 Git에 커밋하고, .received.txt는 최신 실행 결과로 .gitignore에 등록합니다. 소스 생성기를 수정하면 스냅샷 불일치로 테스트가 실패하므로, diff를 검토한 뒤 의도한 변경이면 received를 verified로 승인합니다.
FAQ
섹션 제목: “FAQ”Q1: .verified.txt 파일이 수십 개로 늘어나면 관리가 어렵지 않나요?
섹션 제목: “Q1: .verified.txt 파일이 수십 개로 늘어나면 관리가 어렵지 않나요?”A: 파일 수가 많아지는 것은 사실이지만, 각 파일이 하나의 시나리오에 대한 “정답”을 담고 있으므로 오히려 문서화 역할을 합니다. Git에서 변경된 .verified.txt 파일만 리뷰하면 되고, 의도하지 않은 변경은 CI에서 자동으로 감지합니다. 테스트 이름을 명확하게 작성하면 파일명만으로도 시나리오를 파악할 수 있습니다.
Q2: .received.txt와 .verified.txt의 차이를 빠르게 확인하는 방법은 무엇인가요?
섹션 제목: “Q2: .received.txt와 .verified.txt의 차이를 빠르게 확인하는 방법은 무엇인가요?”A: Verify 라이브러리는 테스트 실패 시 자동으로 diff 도구를 실행하도록 설정할 수 있습니다. Visual Studio에서는 내장 diff 뷰어가 열리고, CLI에서는 diff *.verified.txt *.received.txt 또는 code --diff 명령어로 비교할 수 있습니다. Functorium에서는 Build-VerifyAccept.ps1 스크립트로 모든 pending 스냅샷을 일괄 승인합니다.
Q3: 소스 생성기의 출력이 비결정적이면 스냅샷 테스트가 매번 실패하지 않나요?
섹션 제목: “Q3: 소스 생성기의 출력이 비결정적이면 스냅샷 테스트가 매번 실패하지 않나요?”A: 맞습니다. 그래서 스냅샷 테스트는 결정적 출력을 전제로 합니다. 타임스탬프, GUID, 환경 변수 등 비결정적 요소가 생성 코드에 포함되면 매 실행마다 .received.txt가 달라져 테스트가 지속적으로 실패합니다. 이것이 Part 2의 12장에서 다룬 결정적 출력 원칙이 스냅샷 테스트의 필수 전제인 이유입니다.
테스트 도구가 준비되었으니, ObservablePortGenerator의 31개 테스트 시나리오를 카테고리별로 살펴봅니다.