본문으로 건너뛰기

Verify Snapshot Test

소스 생성기가 만들어내는 코드는 수십~수백 줄에 달합니다. 이를 assertion 하나하나로 검증하면 테스트 코드가 생성 코드보다 더 복잡해집니다. 스냅샷 테스트는 생성된 코드 전체를 .verified.txt 파일로 저장하고, 이후 실행 결과와 자동으로 비교하는 방식입니다. 변경이 생기면 diff를 보여주므로, 의도한 변경인지 실수인지 즉시 판별할 수 있습니다. 소스 생성기처럼 출력이 결정적(deterministic)이고 전체 형태가 중요한 경우에 특히 적합한 테스트 전략입니다.

  1. Verify 라이브러리 이해
    • xUnit과 통합되는 스냅샷 테스트 프레임워크의 동작 방식
  2. .verified.txt 파일 관리
    • 승인(approve) 워크플로우와 소스 제어 전략
  3. 스냅샷 테스트 워크플로우
    • 첫 실행부터 변경 감지, 승인까지의 전체 과정

스냅샷 테스트는 출력 결과를 파일로 저장하고 이후 테스트에서 비교하는 방식입니다.

첫 번째 실행
============
입력 → 생성 → 결과 저장 (*.verified.txt)
이후 실행
=========
입력 → 생성 → 저장된 결과와 비교
├─ 일치 → 테스트 통과
└─ 불일치 → 테스트 실패
└─ *.received.txt 생성 (비교용)

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

{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;
// ... 생성된 전체 코드
}

[Fact]
public Task Should_Generate_NewFeature()
{
string input = """
// 새로운 시나리오 입력
""";
string? actual = _sut.Generate(input);
return Verify(actual);
}
[xUnit.net 00:00:01.23] Should_Generate_NewFeature [FAIL]
Verify returned:
- Received: ...NewFeature.received.txt
- Verified: ...NewFeature.verified.txt (not found)
Terminal window
# 생성된 파일 확인
cat Tests/.../Should_Generate_NewFeature.received.txt
Terminal window
# received.txt를 verified.txt로 이름 변경
mv *.received.txt *.verified.txt
# 또는 도구 사용
dotnet tool run verify.tool accept
[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: _parentContext
Terminal window
# diff 도구 사용
diff *.verified.txt *.received.txt
# Visual Studio Code
code --diff *.verified.txt *.received.txt
Terminal window
# 의도적 변경이면 새 버전 승인
mv *.received.txt *.verified.txt

Terminal window
# verified.txt 파일은 반드시 커밋
git add *.verified.txt
git commit -m "test: update verified snapshots"
.gitignore
*.received.txt
*.received/
# 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);
}
TestNamespace/TestAdapterObservable.g.cs
// 파일명: 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에서 변경된 파일 검토 필요

// ✅ 좋은 예
public Task Should_Generate_ObservableClass_WithPrimaryConstructor()
// ❌ 나쁜 예
public Task Test1()
// ✅ 좋은 예 - 한 가지만 테스트
[Fact]
public Task Should_Generate_Count_ForCollectionParameter() { }
[Fact]
public Task Should_Generate_Length_ForArrayParameter() { }
// ❌ 나쁜 예 - 여러 가지 혼합
[Fact]
public Task Should_Generate_CountAndLength() { }
// ✅ 좋은 예 - 테스트에 필요한 최소 코드
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를 검토한 뒤 의도한 변경이면 receivedverified로 승인합니다.


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개 테스트 시나리오를 카테고리별로 살펴봅니다.

07. 테스트 시나리오