본문으로 건너뛰기

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를 검토한 뒤 의도한 변경이면 received를 verified로 승인합니다.


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. 테스트 시나리오