Functorium.Testing Library Guide
테스트 코드는 프로덕션 코드와 동일한 수준의 일관성이 필요합니다. 프로젝트가 성장하면 로그 캡처, 아키텍처 규칙 검증, 소스 생성기 테스트 등 반복적인 테스트 인프라 코드가 각 프로젝트에 중복됩니다.
Functorium.Testing은 이러한 반복을 제거하고, 프레임워크에 특화된 테스트 유틸리티를 단일 라이브러리로 제공하여 테스트 코드의 일관성과 유지보수성을 확보합니다.
Introduction
Section titled “Introduction”“Pipeline이 출력하는 구조화된 로그 필드가 정확한지 어떻게 검증하는가?” “ValueObject의 불변성 규칙을 모든 클래스에 일괄 적용하려면 어떻게 해야 하는가?” “소스 생성기가 올바른 코드를 생성하는지 어떻게 테스트하는가?”
이러한 테스트 인프라를 프로젝트마다 직접 구현하면 중복 코드가 쌓이고, 프레임워크 업데이트 시 동기화가 어려워집니다. Functorium.Testing은 이러한 반복 패턴을 단일 라이브러리로 통합하여 일관된 테스트 기반을 제공합니다.
What You Will Learn
Section titled “What You Will Learn”This document covers the following topics:
LogTestContext기반 구조화된 로그 테스트 - Serilog 인메모리 캡처와 Verify 스냅샷 연동FinTFactory를 활용한 Mock 반환값 설정 - Port/Adapter의FinT<IO, T>반환값 생성- 아키텍처 규칙 검증 Fluent API - ArchUnitNET 기반 클래스/메서드 수준 규칙 적용
SourceGeneratorTestRunner로 소스 생성기 테스트 - 입력 코드 → 생성 코드 검증QuartzTestFixture로 스케줄 Job 통합 테스트 - DI 통합 환경에서 Job 1회 실행 검증
Prerequisites
Section titled “Prerequisites”A basic understanding of the following concepts is needed to understand this document:
- unit 테스트 가이드 - AAA 패턴, MTP 설정, Verify 스냅샷 테스트
- LanguageExt의
Fin<T>,FinT<IO, T>타입 기본 개념 - Serilog 구조화된 로깅의 기본 원리
Core principle:
Functorium.Testing은 구조화된 로그 캡처, 아키텍처 규칙 검증, 소스 생성기 테스트, Mock 반환값 생성 등 반복적인 테스트 인프라를 단일 라이브러리로 통합하여 프로젝트 간 일관성을 보장합니다.
Summary
Section titled “Summary”Key Commands
Section titled “Key Commands”// 구조화된 로그 테스트using var context = new LogTestContext();var logger = context.CreateLogger<MyPipeline>();// ... 테스트 실행 후await Verify(context.ExtractFirstLogData()).UseDirectory("Snapshots");
// 아키텍처 규칙 검증ArchRuleDefinition.Classes().That() .ImplementInterface(typeof(IValueObject)) .ValidateAllClasses(Architecture, @class => { ... }) .ThrowIfAnyFailures("Rule Name");
// 소스 생성기 테스트string? actual = _sut.Generate(input);return Verify(actual).UseDirectory("Snapshots/EntityIdGenerator");
// Mock 반환값 설정_repository.GetById(Arg.Any<ProductId>()) .Returns(FinTFactory.Succ(product));Key Procedures
Section titled “Key Procedures”1. 로그 테스트:
LogTestContext생성CreateLogger<T>()로 ILogger 생성- 테스트 대상에 로거 주입 후 실행
ExtractFirstLogData()등으로 데이터 추출Verify()로 스냅샷 비교 또는 직접 Assertion
2. 아키텍처 규칙 검증:
ArchRuleDefinition.Classes()로 대상 클래스 필터링ValidateAllClasses()에 검증 규칙 콜백 전달ThrowIfAnyFailures()로 failure 시 예외 발생
Key Concepts
Section titled “Key Concepts”| Concept | Description |
|---|---|
LogTestContext | Serilog 기반 인메모리 로그 캡처 컨텍스트 |
FinTFactory | FinT<IO, T> Mock 반환값 생성 헬퍼 |
ClassValidator | 클래스 수준 아키텍처 규칙 Fluent API |
SourceGeneratorTestRunner | IIncrementalGenerator 테스트 실행기 |
QuartzTestFixture | Quartz.NET Job 통합 테스트 Fixture |
Overview
Section titled “Overview”Functorium.Testing은 Functorium 프레임워크의 테스트 유틸리티 라이브러리입니다.
Namespace Structure
Section titled “Namespace Structure”다음 테이블은 라이브러리의 전체 네임스페이스 구조와 각 모듈의 역할을 정리한 것입니다.
| 네임스페이스 | 역할 |
|---|---|
Functorium.Testing.Arrangements.Logging | 구조화된 로그 캡처 (LogTestContext, StructuredTestLogger) |
Functorium.Testing.Arrangements.Loggers | 인메모리 Serilog Sink (TestSink) |
Functorium.Testing.Arrangements.Effects | FinT<IO, T> 반환값 생성 헬퍼 (FinTFactory) |
Functorium.Testing.Arrangements.Hosting | HTTP 통합 테스트 Fixture (HostTestFixture) |
Functorium.Testing.Arrangements.ScheduledJobs | 스케줄 Job 테스트 Fixture (QuartzTestFixture) |
Functorium.Testing.Actions.SourceGenerators | 소스 생성기 테스트 Runner |
Functorium.Testing.Assertions.ArchitectureRules | 아키텍처 규칙 검증 (ClassValidator, MethodValidator, InterfaceValidator) |
Functorium.Testing.Assertions.ArchitectureRules.Rules | 재사용 가능 규칙 (ImmutabilityRule 등) |
Functorium.Testing.Assertions.ArchitectureRules.Suites | 도메인/Application 아키텍처 테스트 스위트 (DomainArchitectureTestSuite, ApplicationArchitectureTestSuite) |
Functorium.Testing.Assertions.Logging | 로그 데이터 추출/변환 유틸리티 (SerilogTestPropertyValueFactory 포함) |
Functorium.Testing.Assertions.Errors | 에러 타입 Assertion (Domain/Application/Adapter별 + 범용 ErrorCode/Exceptional) |
Features Documented in Other Guides
Section titled “Features Documented in Other Guides”| Feature | 참조 가이드 |
|---|---|
HostTestFixture<TProgram> — HTTP 엔드포인트 통합 테스트 | 15b-integration-testing.md, 01-project-structure.md |
ShouldBeDomainError, ShouldBeApplicationError 등 에러 Assertion | 08b-error-system-domain-app.md, 08c-error-system-adapter-testing.md |
Project Reference Setup
Section titled “Project Reference Setup”unit 테스트 csproj 패키지 구성
Section titled “unit 테스트 csproj 패키지 구성”<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup>
<ItemGroup> <!-- 테스트 프레임워크 --> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio" /> <PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" /> <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
<!-- Assertion / Mocking --> <PackageReference Include="Shouldly" /> <PackageReference Include="NSubstitute" /> <PackageReference Include="Verify.XunitV3" />
<!-- 로그 테스트 --> <PackageReference Include="Serilog" />
<!-- 소스 생성기 테스트 --> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" /> </ItemGroup>
<ItemGroup> <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\..\Src\MyProject\MyProject.csproj" /> <ProjectReference Include="..\..\Src\Functorium.Testing\Functorium.Testing.csproj" /> </ItemGroup>
</Project>Source Generator Dual Reference Pattern
Section titled “Source Generator Dual Reference Pattern”소스 생성기 프로젝트를 테스트할 때는 두 가지 참조가 모두 필요합니다.
<!-- 1. 일반 참조: 생성기 타입(클래스)을 코드에서 사용하기 위한 참조 --><ItemGroup> <ProjectReference Include="..\..\Src\MyProject.SourceGenerator\MyProject.SourceGenerator.csproj" /></ItemGroup>
<!-- 2. Analyzer 참조: 소스 생성기가 실제 코드 생성을 수행하도록 활성화 --><ItemGroup> <ProjectReference Include="..\..\Src\MyProject.SourceGenerator\MyProject.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /></ItemGroup>| 참조 방식 | Purpose |
|---|---|
일반 ProjectReference | 생성기 타입을 new EntityIdGenerator()처럼 인스턴스화 |
OutputItemType="Analyzer" | 빌드 시 [GenerateEntityId] 등의 어트리뷰트로 코드 생성 활성화 |
Note: 통합 테스트에서 Host 프로젝트를 참조할 때 Mediator SourceGenerator 중복을 방지하려면
ExcludeAssets="analyzers"를 추가합니다. 자세한 내용은 01-project-structure.md의 FAQ를 참조하세요.
Recommended Using.cs Pattern
Section titled “Recommended Using.cs Pattern”global using Functorium.Testing.Arrangements.Logging;global using Functorium.Testing.Assertions.Logging;global using Functorium.Testing.Actions.SourceGenerators;global using Functorium.Testing.Assertions.ArchitectureRules;global using Xunit;global using Shouldly;프로젝트 참조가 구성되었으면, 이제 라이브러리가 제공하는 핵심 기능을 하나씩 살펴봅니다.
FinTFactory (Mock 반환값 헬퍼)
Section titled “FinTFactory (Mock 반환값 헬퍼)”FinTFactory는 FinT<IO, T> 반환값을 간편하게 생성하는 정적 헬퍼입니다. Port/Adapter의 Mock 반환값을 설정할 때 사용합니다.
// 네임스페이스using Functorium.Testing.Arrangements.Effects;| 메서드 | 반환 타입 | Description |
|---|---|---|
FinTFactory.Succ<T>(T value) | FinT<IO, T> | success 값을 래핑한 FinT 생성 |
FinTFactory.Fail<T>(Error error) | FinT<IO, T> | failure 에러를 래핑한 FinT 생성 |
NSubstitute Usage Example
Section titled “NSubstitute Usage Example”// Port Mock 설정 — success 반환_productRepository .GetById(Arg.Any<ProductId>()) .Returns(FinTFactory.Succ(product));
// Port Mock 설정 — failure 반환_productRepository .GetById(Arg.Any<ProductId>()) .Returns(FinTFactory.Fail<Product>( AdapterError.For<InMemoryProductRepository>( new NotFound(), id.ToString(), "상품을 찾을 수 없습니다")));Structured Log Testing
Section titled “Structured Log Testing”구조화된 로그 테스트는 LoggerMessage 어트리뷰트 기반의 로깅이 올바른 필드 구조를 출력하는지 검증합니다.
Components
Section titled “Components”LogTestContext (테스트 진입점)├── StructuredTestLogger<T> ← ILogger<T> 구현 (Serilog 브릿지)├── TestSink ← 인메모리 Serilog Sink└── LogEventPropertyExtractor / LogEventPropertyValueConverter ← 데이터 추출LogTestContext
Section titled “LogTestContext”로그 테스트의 핵심 컨텍스트입니다. 생성 시 내부적으로 Serilog Logger + TestSink을 구성하고, CreateLogger<T>()로 ILogger<T>를 생성합니다.
// 네임스페이스using Functorium.Testing.Arrangements.Logging;Construction
Section titled “Construction”// 기본 (최소 레벨: Debug)using var context = new LogTestContext();
// 최소 레벨 지정using var context = new LogTestContext(LogEventLevel.Information);CreateLogger<T>()
Section titled “CreateLogger<T>()”ILogger<T> 인스턴스를 생성합니다. 이 로거로 기록된 로그는 모두 컨텍스트에 캡처됩니다.
var logger = context.CreateLogger<MyPipeline>();Log Query API
Section titled “Log Query API”| Method | Description |
|---|---|
LogEvents | 캡처된 전체 LogEvent 목록 (IReadOnlyList) |
LogCount | 캡처된 로그 수 |
GetFirstLog() | 첫 번째 로그 (일반적으로 Request 로그) |
GetSecondLog() | 두 번째 로그 (일반적으로 Response 로그) |
GetLogAt(int index) | 인덱스로 로그 조회 |
GetLogsByLevel(LogEventLevel level) | 특정 레벨의 로그 목록 |
Clear() | 캡처된 로그 전체 삭제 |
Data Extraction API
Section titled “Data Extraction API”Verify 스냅샷 테스트용으로 LogEvent를 익명 객체로 변환합니다.
| Method | Description |
|---|---|
ExtractFirstLogData() | 첫 번째 로그 데이터를 익명 객체로 추출 |
ExtractSecondLogData() | 두 번째 로그 데이터를 익명 객체로 추출 |
ExtractLogDataAt(int index) | 인덱스 지정 로그 데이터 추출 |
ExtractAllLogData() | 전체 로그 데이터를 익명 객체 목록으로 추출 |
StructuredTestLogger<T>
Section titled “StructuredTestLogger<T>”ILogger<T> → Serilog 브릿지 역할을 합니다. LoggerMessage 어트리뷰트로 생성된 구조화된 로깅을 올바르게 처리합니다.
IReadOnlyList<KeyValuePair<string, object?>>형태의 state에서{OriginalFormat}과 attribute들을 분리{@Error:Error}형태의 명시적 attribute명을 처리LogEvent를 직접 생성하여 attribute명을 정확하게 유지
Caution:
LogTestContext.CreateLogger<T>()를 통해 생성하세요. 직접 인스턴스화할 필요는 없습니다.
TestSink
Section titled “TestSink”인메모리 Serilog ILogEventSink 구현입니다. LogTestContext가 내부적으로 사용하며, 직접 사용할 일은 거의 없습니다.
// 네임스페이스using Functorium.Testing.Arrangements.Loggers;LogEventPropertyExtractor
Section titled “LogEventPropertyExtractor”LogEvent에서 attribute 값을 재귀적으로 추출하는 유틸리티입니다.
// 네임스페이스using Functorium.Testing.Assertions.Logging;| Method | Description |
|---|---|
ExtractValue(LogEventPropertyValue) | ScalarValue, SequenceValue, StructureValue, DictionaryValue를 재귀적으로 추출 |
ExtractLogData(LogEvent) | 단일 LogEvent → { Information, Properties } 익명 객체 |
ExtractLogData(IEnumerable<LogEvent>) | 여러 LogEvent → 익명 객체 목록 |
SerilogTestPropertyValueFactory
Section titled “SerilogTestPropertyValueFactory”테스트 환경에서 LogEvent를 수동 생성할 때 프로퍼티 값을 Serilog LogEventPropertyValue로 변환하는 팩토리입니다. ILogEventPropertyValueFactory 구현체로, string, int, long, double, bool, Exception, ValueTuple 등 주요 타입을 지원합니다.
using Functorium.Testing.Assertions.Logging;
var factory = new SerilogTestPropertyValueFactory();var value = factory.CreatePropertyValue("test-value");LogEventPropertyValueConverter
Section titled “LogEventPropertyValueConverter”LogEventPropertyValue를 Verify 스냅샷용 익명 객체로 변환합니다.
| Method | Description |
|---|---|
ToAnonymousObject(LogEventPropertyValue) | StructureValue → Dictionary, SequenceValue → Array, ScalarValue → 원시값 |
LogEventPropertyExtractor Type-Specific Processing Details
Section titled “LogEventPropertyExtractor Type-Specific Processing Details”LogEventPropertyExtractor는 static class이며, ExtractValue(LogEventPropertyValue) 메서드에서 switch 식으로 Serilog의 모든 주요 LogEventPropertyValue 하위 타입을 처리합니다.
타입별 처리 로직:
| Type | 처리 방식 | 결과 |
|---|---|---|
ScalarValue | .Value (null이면 "null" 문자열) | 원시 값 (string, int, bool 등) |
SequenceValue | .Elements.Select(ExtractValue).ToList() | List<object> |
StructureValue | .Properties.ToDictionary(p => p.Name, p => ExtractValue(p.Value)) | Dictionary<string, object> |
DictionaryValue | .Elements.ToDictionary(kvp => kvp.Key.Value?.ToString() ?? "null", kvp => ExtractValue(kvp.Value)) | Dictionary<string, object> |
| 기타 | HandleUnhandledType() — Debug.WriteLine 후 .ToString() 반환 | string |
ExtractLogData(LogEvent) — 단일 LogEvent에서 익명 객체를 생성합니다:
new{ Information = logEvent.MessageTemplate.Text, Properties = logEvent.Properties.ToDictionary( static p => p.Key, static p => ExtractValue(p.Value) )}ExtractLogData(IEnumerable<LogEvent>) — 여러 LogEvent를 .Select()로 변환합니다.
Note: 정적 람다(
static p =>)를 사용하여 불필요한 클로저 할당을 방지합니다.
LogEventPropertyExtractor Usage Example
Section titled “LogEventPropertyExtractor Usage Example”스냅샷 테스트가 아닌 직접 Assertion으로 로그 필드를 검증하는 패턴:
[Fact]public async Task Pipeline_Should_Log_RequestLayer_And_Handler(){ // Arrange using var context = new LogTestContext(); var logger = context.CreateLogger<UsecaseLoggingPipeline<TestRequest, TestResponse>>(); var pipeline = new UsecaseLoggingPipeline<TestRequest, TestResponse>(logger);
// Act await pipeline.Handle(new TestRequest("Test"), next, CancellationToken.None);
// Assert - 첫 번째 로그의 attribute을 직접 검증 var firstLog = context.GetFirstLog(); var data = LogEventPropertyExtractor.ExtractLogData(firstLog);
// Properties에서 특정 필드 검증 var properties = (IDictionary<string, object?>)data.Properties; properties["request.layer"].ShouldBe("application"); properties["request.category.name"].ShouldBe("usecase"); properties["request.handler.name"].ShouldNotBeNull();}Verify Snapshot Integration Pattern
Section titled “Verify Snapshot Integration Pattern”[Fact]public async Task Command_Request_Should_Log_Expected_Fields(){ // Arrange using var context = new LogTestContext(); var logger = context.CreateLogger<UsecaseLoggingPipeline<TestCommandRequest, TestResponse>>(); var pipeline = new UsecaseLoggingPipeline<TestCommandRequest, TestResponse>(logger); var request = new TestCommandRequest("TestName"); var expectedResponse = TestResponse.CreateSuccess(Guid.NewGuid());
MessageHandlerDelegate<TestCommandRequest, TestResponse> next = (_, _) => ValueTask.FromResult(expectedResponse);
// Act await pipeline.Handle(request, next, CancellationToken.None);
// Assert - 첫 번째 로그(Request)의 필드 구조를 스냅샷으로 검증 await Verify(context.ExtractFirstLogData()).UseDirectory("Snapshots");}핵심 흐름:
LogTestContext생성CreateLogger<T>()로 로거 생성- 테스트 대상 코드에 로거 주입 후 실행
ExtractFirstLogData()/ExtractAllLogData()등으로 데이터 추출Verify()로 스냅샷 비교
Mock 반환값 설정 방법을 익혔으면, 다음으로 아키텍처 규칙을 자동으로 검증하는 방법을 알아봅니다.
Architecture Rule Validation
Section titled “Architecture Rule Validation”ArchUnitNET 기반으로 클래스/메서드 수준의 아키텍처 규칙을 Fluent API로 검증합니다.
// 네임스페이스using Functorium.Testing.Assertions.ArchitectureRules;ArchitectureValidationEntryPoint.ValidateAllClasses()
Section titled “ArchitectureValidationEntryPoint.ValidateAllClasses()”ArchUnitNET의 IObjectProvider<Class>에 대한 확장 메서드입니다. 필터링된 클래스 집합에 대해 검증 규칙을 일괄 적용합니다.
public static ValidationResultSummary ValidateAllClasses( this IObjectProvider<Class> classes, Architecture architecture, Action<ClassValidator> validationRule, bool verbose = false);ClassValidator Fluent API
Section titled “ClassValidator Fluent API”가시성:
| Method | Description |
|---|---|
RequirePublic() | public 클래스여야 함 |
RequireInternal() | internal 클래스여야 함 |
한정자:
| Method | Description |
|---|---|
RequireSealed() / RequireNotSealed() | sealed 여부 |
RequireStatic() / RequireNotStatic() | static 여부 |
RequireAbstract() / RequireNotAbstract() | abstract 여부 |
네이밍 (TypeValidator에서 상속):
| Method | Description |
|---|---|
RequireNameStartsWith(string) | 이름이 특정 접두사로 시작해야 함 |
RequireNameEndsWith(string) | 이름이 특정 접미사로 끝나야 함 |
RequireNameMatching(string) | 이름이 정규식 패턴과 일치해야 함 |
타입/상속:
| Method | Description |
|---|---|
RequireRecord() / RequireNotRecord() | record 타입 여부 |
RequireAttribute(string) | 특정 어트리뷰트 적용 필수 |
RequireInherits(Type) | 특정 기본 클래스 상속 필수 |
RequireImplements(Type) | 특정 인터페이스 구현 필수 |
RequireImplementsGenericInterface(string) | 제네릭 인터페이스 구현 필수 |
RequireNoDependencyOn(string) | 특정 타입에 대한 의존 금지 |
생성자/프로퍼티/필드:
| Method | Description |
|---|---|
RequireAllPrivateConstructors() | 모든 생성자가 private이어야 함 |
RequirePrivateAnyParameterlessConstructor() | 매개변수 없는 private 생성자 필수 |
RequireNoPublicSetters() | public setter 금지 (get-only만 허용) |
RequireOnlyPrimitiveProperties(params string[]) | 원시 타입 프로퍼티만 허용 (추가 허용 타입 지정 가능) |
RequireNoInstanceFields(params string[]) | 인스턴스 필드 금지 (제외할 필드 타입 지정 가능) |
RequireImmutable() | 불변성 종합 검증 (6가지 차원) |
메서드/중첩 클래스:
| Method | Description |
|---|---|
RequireMethod(string, Action<MethodValidator>) | 특정 이름의 메서드 검증 |
RequireMethodIfExists(string, Action<MethodValidator>) | 메서드가 있으면 검증 |
RequireAllMethods(Action<MethodValidator>) | 모든 메서드에 대해 검증 |
RequireProperty(string) | 특정 이름의 프로퍼티 필수 |
RequireNestedClass(string, Action<ClassValidator>?) | 중첩 클래스 필수 + 검증 |
RequireNestedClassIfExists(string, Action<ClassValidator>?) | 중첩 클래스가 있으면 검증 |
ValidateAndThrow() | 단일 클래스 검증 후 즉시 예외 |
RequireImmutable() 검증 항목
Section titled “RequireImmutable() 검증 항목”RequireImmutable()은 ValueObject의 불변성을 6가지 차원에서 종합 검증합니다:
- Writability 검증 — 모든 non-static 멤버가
IsImmutable()을 만족 - 생성자 검증 — 모든 생성자가 private (public 생성자 금지)
- 프로퍼티 검증 — public setter 금지 (get-only만 허용)
- 필드 검증 — public 필드 금지 (모든 필드는 private)
- 가변 컬렉션 검증 —
List<T>,Dictionary<K,V>,HashSet<T>등 금지 - 상태 변경 메서드 검증 —
Set*,Update*,Add*,Remove*등 금지
MethodValidator Fluent API
Section titled “MethodValidator Fluent API”가시성/한정자:
| Method | Description |
|---|---|
RequireVisibility(Visibility) | 특정 가시성 필수 |
RequireStatic() / RequireNotStatic() | static 여부 |
RequireVirtual() / RequireNotVirtual() | virtual 여부 |
RequireExtensionMethod() | 확장 메서드여야 함 |
반환 타입:
| Method | Description |
|---|---|
RequireReturnType(Type) | 반환 타입 검증 (제네릭 타입 매칭 지원) |
RequireReturnTypeOfDeclaringClass() | 선언 클래스를 반환해야 함 |
RequireReturnTypeOfDeclaringTopLevelClass() | 최상위 선언 클래스를 반환해야 함 |
RequireReturnTypeContaining(string) | 반환 타입 이름에 특정 문자열 포함 |
매개변수:
| Method | Description |
|---|---|
RequireParameterCount(int) | 정확한 매개변수 개수 |
RequireParameterCountAtLeast(int) | 최소 매개변수 개수 |
RequireFirstParameterTypeContaining(string) | 첫 번째 매개변수 타입에 특정 문자열 포함 |
RequireAnyParameterTypeContaining(string) | 임의 매개변수 타입에 특정 문자열 포함 |
InterfaceValidator
Section titled “InterfaceValidator”InterfaceValidator는 TypeValidator<Interface, InterfaceValidator>를 상속하며, ClassValidator와 동일한 Fluent API 패턴을 인터페이스에 적용합니다.
IArchRule<T> 인터페이스
Section titled “IArchRule<T> 인터페이스”재사용 가능한 아키텍처 규칙을 정의하는 인터페이스입니다.
| Type | Description |
|---|---|
IArchRule<TType> | 규칙 인터페이스. Description과 Validate() 메서드 제공 |
DelegateArchRule<TType> | 람다 기반 규칙 구현 |
CompositeArchRule<TType> | 여러 규칙을 AND로 합성 |
ImmutabilityRule | 클래스 불변성 검증 규칙 (14개 가변 컬렉션 타입 감지) |
Architecture Test Suite
Section titled “Architecture Test Suite”DomainArchitectureTestSuite와 ApplicationArchitectureTestSuite는 도메인/Application 레이어의 아키텍처 규칙을 사전 정의된 테스트 집합으로 제공합니다. 상속하여 Architecture와 네임스페이스만 지정하면 21+4개의 아키텍처 테스트가 자동 적용됩니다.
ArchitectureTestBase Setup
Section titled “ArchitectureTestBase Setup”ArchLoader로 검사 대상 어셈블리를 로드하고, 공유 상수로 네임스페이스를 정의합니다:
using ArchUnitNET.Loader;
internal static class ArchitectureTestBase{ internal static readonly ArchUnitNET.Domain.Architecture Architecture = new ArchLoader() .LoadAssemblies( typeof(Functorium.Domains.Specifications.Specification<>).Assembly, ECommerce.Domain.AssemblyReference.Assembly, ECommerce.Application.AssemblyReference.Assembly) .Build();
internal static readonly string DomainNamespace = typeof(ECommerce.Domain.AssemblyReference).Namespace!; internal static readonly string ApplicationNamespace = typeof(ECommerce.Application.AssemblyReference).Namespace!;}DomainArchitectureRuleTests
Section titled “DomainArchitectureRuleTests”using Functorium.Testing.Assertions.ArchitectureRules.Suites;
public sealed class DomainArchitectureRuleTests : DomainArchitectureTestSuite{ protected override ArchUnitNET.Domain.Architecture Architecture => ArchitectureTestBase.Architecture; protected override string DomainNamespace => ArchitectureTestBase.DomainNamespace;}ApplicationArchitectureRuleTests
Section titled “ApplicationArchitectureRuleTests”using Functorium.Testing.Assertions.ArchitectureRules.Suites;
public sealed class ApplicationArchitectureRuleTests : ApplicationArchitectureTestSuite{ protected override ArchUnitNET.Domain.Architecture Architecture => ArchitectureTestBase.Architecture; protected override string ApplicationNamespace => ArchitectureTestBase.ApplicationNamespace;}Customization: Overridable Properties
Section titled “Customization: Overridable Properties”특정 도메인 구조에 맞게 테스트 스위트를 조정할 수 있습니다:
public sealed class DomainArchitectureRuleTests : DomainArchitectureTestSuite{ private static readonly ArchUnitNET.Domain.Architecture s_architecture = new ArchLoader() .LoadAssemblies( typeof(Functorium.Domains.Specifications.Specification<>).Assembly, DesigningWithTypes.AssemblyReference.Assembly) .Build();
protected override ArchUnitNET.Domain.Architecture Architecture => s_architecture; protected override string DomainNamespace => typeof(DesigningWithTypes.AssemblyReference).Namespace!;
// Union VO는 Create/Validate 팩토리 패턴 불필요 → 검사에서 제외 protected override IReadOnlyList<Type> ValueObjectExcludeFromFactoryMethods => [typeof(UnionValueObject)];
// DomainService가 Repository 필드를 가질 수 있도록 허용 protected override string[] DomainServiceAllowedFieldTypes => ["Repository"];}| override attribute | Default | Purpose |
|---|---|---|
ValueObjectExcludeFromFactoryMethods | [] | Create/Validate 팩토리 검사에서 제외할 VO 타입. Union VO처럼 팩토리 없이 직접 생성하는 타입에 사용 |
DomainServiceAllowedFieldTypes | [] | DomainService 필드 타입 허용 목록. Repository를 주입받는 DomainService에 사용 |
DomainArchitectureTestSuite (21개 테스트): AggregateRoot, Entity, ValueObject, DomainEvent, Specification, DomainService에 대한 아키텍처 규칙을 자동 검증합니다.
ApplicationArchitectureTestSuite (4개 테스트): Command/Query의 Validator, Usecase 중첩 클래스 존재를 자동 검증합니다.
ValidationResultSummary.ThrowIfAnyFailures()
Section titled “ValidationResultSummary.ThrowIfAnyFailures()”여러 클래스의 검증 결과를 집계한 후 failure가 있으면 XunitException을 발생시킵니다.
summary.ThrowIfAnyFailures("ValueObject Immutability Rule");예외 메시지 형식:
'ValueObject Immutability Rule' rule violation:
MyProject.ValueObjects.Email: - Class 'Email' must be sealed. - Found public constructors: .ctor
MyProject.ValueObjects.PhoneNumber: - Method 'Create' in class 'PhoneNumber' must be static.SingleHost Architecture Test Inventory
Section titled “SingleHost Architecture Test Inventory”다음 테이블은 SingleHost 레퍼런스 프로젝트에 구현된 아키텍처 테스트의 전체 목록입니다.
| 테스트 클래스 | 테스트 수 | 검증 대상 |
|---|---|---|
LayerDependencyArchitectureRuleTests | 6 | 레이어 간 의존성 방향 (Domain !→ Application, Adapter 간 교차 참조 금지 등) |
EntityArchitectureRuleTests | 5 | AggregateRoot/Entity: public sealed, 상속, Create/CreateFromValidated 팩토리 |
ValueObjectArchitectureRuleTests | 4 | ValueObject: public sealed, 불변성, Create/Validate 팩토리 |
DtoArchitectureRuleTests | 5 | DTO/Model/Mapper: Persistence Mapper internal static, Usecase nested Request/Response |
CqrsArchitectureRuleTests | 1 | CQRS 패턴 준수: Query Usecase가 IRepository에 의존하지 않도록 강제 |
UsecaseArchitectureRuleTests | 4 | Command/Query: 내부 Validator/Usecase nested class 존재 |
SpecificationArchitectureRuleTests | 3 | Specification: public sealed, 상속, Domain 레이어 거주 |
PortAndAdapterArchitectureRuleTests | 3 | Adapter: GenerateObservablePort 어트리뷰트, RequestCategory, DomainService sealed |
Usage Pattern: ValueObject Immutability Validation
Section titled “Usage Pattern: ValueObject Immutability Validation”[Fact]public void ValueObject_ShouldSatisfy_ImmutabilityRules(){ ArchRuleDefinition .Classes() .That() .ImplementInterface(typeof(IValueObject)) .And() .AreNotAbstract() .ValidateAllClasses(Architecture, @class => { // 클래스 수준 검증 @class .RequirePublic() .RequireSealed() .RequireAllPrivateConstructors() .RequireImmutable() .RequireImplements(typeof(IEquatable<>));
// Create 메서드 검증 @class.RequireMethod("Create", method => method .RequireVisibility(Visibility.Public) .RequireStatic() .RequireReturnType(typeof(Fin<>)));
// Validate 메서드 검증 @class.RequireMethod("Validate", method => method .RequireVisibility(Visibility.Public) .RequireStatic() .RequireReturnType(typeof(Validation<,>)));
// DomainErrors 중첩 클래스 검증 (존재하는 경우만) @class.RequireNestedClassIfExists("DomainErrors", domainErrors => { domainErrors .RequireInternal() .RequireSealed() .RequireAllMethods(method => method .RequireVisibility(Visibility.Public) .RequireStatic() .RequireReturnType(typeof(Error))); }); }) .ThrowIfAnyFailures("ValueObject Rule");}아키텍처 규칙은 클래스 구조를 검증한다면, 소스 생성기 테스트는 코드 생성 결과를 검증합니다.
Source Generator Testing
Section titled “Source Generator Testing”SourceGeneratorTestRunner는 IIncrementalGenerator를 테스트 환경에서 실행하고 생성된 코드를 반환합니다. EntityIdGenerator, ObservablePortGenerator, UnionTypeGenerator 모두 동일한 패턴으로 테스트할 수 있습니다.
// 네임스페이스using Functorium.Testing.Actions.SourceGenerators;SourceGeneratorTestRunner.Generate<TGenerator>()
Section titled “SourceGeneratorTestRunner.Generate<TGenerator>()”소스 코드를 입력받아 소스 생성기를 실행하고 생성된 코드 문자열을 반환합니다.
public static string? Generate<TGenerator>(this TGenerator generator, string sourceCode) where TGenerator : IIncrementalGenerator, new();내부적으로 다음을 수행합니다:
- 입력 소스 코드를
CSharpSyntaxTree로 파싱 - 필수 어셈블리 참조 자동 추가 (System.Runtime, LanguageExt.Core, Microsoft.Extensions.Logging)
CSharpGeneratorDriver로 소스 생성기 실행- 컴파일러 에러가 있으면 Shouldly assertion으로 failure
- 생성된 코드 반환 (생성되지 않은 경우
null)
GenerateWithDiagnostics<TGenerator>()
Section titled “GenerateWithDiagnostics<TGenerator>()”진단 결과(Diagnostic)를 함께 반환합니다. DiagnosticDescriptor 테스트에 사용합니다.
public static (string? GeneratedCode, ImmutableArray<Diagnostic> Diagnostics) GenerateWithDiagnostics<TGenerator>(this TGenerator generator, string sourceCode) where TGenerator : IIncrementalGenerator, new();Verify Snapshot Comparison Pattern
Section titled “Verify Snapshot Comparison Pattern”[Fact]public Task EntityIdGenerator_ShouldGenerate_EntityId_ForSimpleEntity(){ // Arrange string input = """ using Functorium.Domains.Entities;
namespace MyApp.Domain.Entities;
[GenerateEntityId] public class Product { public string Name { get; set; } = string.Empty; } """;
// Act string? actual = _sut.Generate(input);
// Assert return Verify(actual).UseDirectory("Snapshots/EntityIdGenerator");}Validating Attribute Generation with Empty Input
Section titled “Validating Attribute Generation with Empty Input”소스 생성기가 마커 Attribute를 자동 생성하는 경우, 빈 문자열 입력으로 검증합니다:
[Fact]public Task EntityIdGenerator_ShouldGenerate_GenerateEntityIdAttribute(){ // Arrange string input = string.Empty;
// Act string? actual = _sut.Generate(input);
// Assert return Verify(actual).UseDirectory("Snapshots/EntityIdGenerator");}소스 생성기가 정적 코드 생성을 검증한다면, 스케줄 Job 테스트는 런타임에서 실제 Job 실행을 검증합니다.
Scheduled Job Integration Testing
Section titled “Scheduled Job Integration Testing”Quartz.NET Job을 통합 테스트하기 위한 Fixture입니다.
// 네임스페이스using Functorium.Testing.Arrangements.ScheduledJobs;QuartzTestFixture<TProgram>
Section titled “QuartzTestFixture<TProgram>”WebApplicationFactory를 사용하여 전체 DI 설정을 재사용하는 제네릭 Fixture입니다.
Key Properties
Section titled “Key Properties”| attribute | Type | Description |
|---|---|---|
Services | IServiceProvider | DI 컨테이너 |
Scheduler | IScheduler | Quartz 스케줄러 |
JobListener | JobCompletionListener | Job 완료 추적 리스너 |
Environment Configuration
Section titled “Environment Configuration”기본 환경은 "Test"입니다. 파생 클래스에서 오버라이드할 수 있습니다.
// appsettings.Test.json이 자동으로 로드됩니다protected virtual string EnvironmentName => "Test";Note:
appsettings.Test.json파일은 Host 프로젝트 루트에 위치해야 하며,.csproj에서CopyToOutputDirectory를 설정해야 합니다:<ItemGroup><Content Include="appsettings.Test.json" CopyToOutputDirectory="PreserveNewest" /></ItemGroup>
WebApplicationFactory가 Host 프로젝트의ContentRootPath를 기준으로 설정 파일을 로드하므로, 테스트 프로젝트가 아닌 Host 프로젝트에 파일이 있어야 합니다.
DI Extension Point
Section titled “DI Extension Point”ConfigureWebHost를 오버라이드하여 추가 설정을 적용할 수 있습니다.
public class MyJobTestFixture : QuartzTestFixture<Program>{ protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // Replace with test service }); }}ExecuteJobOnceAsync<TJob>()
Section titled “ExecuteJobOnceAsync<TJob>()”지정된 Job을 즉시 1회 실행하고 완료를 대기합니다.
// Job 타입에서 이름/그룹 자동 추출Task<JobExecutionResult> ExecuteJobOnceAsync<TJob>(TimeSpan timeout) where TJob : IJob;
// 이름/그룹 명시적 지정Task<JobExecutionResult> ExecuteJobOnceAsync<TJob>( string jobName, string jobGroup, TimeSpan timeout) where TJob : IJob;내부 동작:
JobListener.Reset()호출- 고유 이름의 테스트용 Job 생성 (
{JobName}-Test-{Guid}) SimpleTrigger로 즉시 1회 실행 스케줄링JobListener.WaitForJobCompletionAsync()로 완료 대기
JobCompletionListener
Section titled “JobCompletionListener”IJobListener 구현체로, Job 완료를 비동기적으로 추적합니다.
| Method | Description |
|---|---|
WaitForJobCompletionAsync(jobName, timeout) | Job 완료 대기 (타임아웃 시 TimeoutException) |
Reset() | 추적 상태 초기화 (각 테스트 전 호출) |
내부적으로 ConcurrentDictionary<string, TaskCompletionSource<JobExecutionResult>>를 사용하여 스레드 안전하게 완료를 추적합니다.
JobExecutionResult
Section titled “JobExecutionResult”Job 실행 결과를 나타내는 record입니다.
| attribute | Type | Description |
|---|---|---|
JobName | string | Job 이름 |
Success | bool | success 여부 |
Result | object? | Job 실행 결과 |
Exception | JobExecutionException? | 발생한 예외 |
ExecutionTime | TimeSpan | 실행 시간 |
Usage Example
Section titled “Usage Example”public sealed class MyJobTests : IAsyncLifetime{ private readonly QuartzTestFixture<Program> _fixture = new();
public ValueTask InitializeAsync() => _fixture.InitializeAsync(); public ValueTask DisposeAsync() => _fixture.DisposeAsync();
[Fact] public async Task MyJob_ShouldComplete_Successfully() { // Act var result = await _fixture.ExecuteJobOnceAsync<MyJob>( timeout: TimeSpan.FromSeconds(10));
// Assert result.Success.ShouldBeTrue(); result.Exception.ShouldBeNull(); }}Timeout Handling Pattern
Section titled “Timeout Handling Pattern”[Fact]public async Task SlowJob_ShouldComplete_WithinTimeout(){ // Act & Assert var result = await _fixture.ExecuteJobOnceAsync<SlowJob>( timeout: TimeSpan.FromSeconds(30));
result.Success.ShouldBeTrue(); result.ExecutionTime.ShouldBeLessThan(TimeSpan.FromSeconds(30));}
[Fact]public async Task Job_ShouldThrow_WhenTimeout(){ // Act & Assert await Should.ThrowAsync<TimeoutException>(async () => await _fixture.ExecuteJobOnceAsync<VerySlowJob>( timeout: TimeSpan.FromSeconds(1)));}Troubleshooting
Section titled “Troubleshooting”소스 생성기 테스트에서 컴파일 에러 발생
Section titled “소스 생성기 테스트에서 컴파일 에러 발생”Cause: SourceGeneratorTestRunner.Generate()는 내부적으로 필수 어셈블리(System.Runtime, LanguageExt.Core, Microsoft.Extensions.Logging)만 자동 참조합니다. 테스트 입력 코드가 다른 어셈블리의 타입을 사용하면 컴파일 에러가 발생합니다.
Resolution: 소스 생성기 테스트의 입력 코드는 자동 참조되는 어셈블리 범위 내에서 작성하세요. 소스 생성기가 처리하는 마커 Attribute와 대상 클래스만 포함하면 충분합니다.
LogTestContext에서 로그가 캡처되지 않음
Section titled “LogTestContext에서 로그가 캡처되지 않음”Cause: LogTestContext의 기본 최소 레벨은 Debug입니다. 테스트 대상이 Verbose 레벨로 로깅하는 경우 캡처되지 않습니다. 또는 CreateLogger<T>()의 타입 파라미터가 실제 로깅 클래스와 다른 경우입니다.
Resolution: 최소 레벨을 명시적으로 지정하세요: new LogTestContext(LogEventLevel.Verbose). 로거의 타입 파라미터가 테스트 대상 클래스의 ILogger<T>와 일치하는지 확인하세요.
아키텍처 규칙 검증에서 예상치 못한 클래스가 포함됨
Section titled “아키텍처 규칙 검증에서 예상치 못한 클래스가 포함됨”Cause: ArchRuleDefinition.Classes().That() 필터 조건이 너무 넓어 의도하지 않은 클래스(추상 클래스, 테스트용 클래스 등)가 포함된 경우입니다.
Resolution: .And().AreNotAbstract(), .And().DoNotHaveNameContaining("Test") 등 추가 필터 조건을 적용하여 대상 범위를 좁히세요. verbose: true 옵션으로 검증 대상 클래스 목록을 확인할 수 있습니다.
Q: LogTestContext와 ITestOutputHelper 차이는?
LogTestContext는 Serilog 기반으로 구조화된 로그 필드(attribute명, 값 타입, 중첩 구조)까지 캡처하여 스냅샷 테스트가 가능합니다. ITestOutputHelper는 단순 텍스트 출력만 지원하므로 필드 구조 검증에는 적합하지 않습니다.
Q: ArchitectureRules를 커스텀할 수 있는가?
가능합니다. 기본 제공 규칙(RequireImmutable, RequireSealed 등) 외에 ValidateAllClasses의 Action<ClassValidator> 콜백에서 프로젝트별 규칙을 조합하여 추가할 수 있습니다.
Q: QuartzTestFixture에서 실제 Job이 실행되는가?
인메모리 스케줄러에서 Job이 실제로 실행됩니다. DI 컨테이너의 모든 서비스가 주입되므로, 외부 의존성(DB, API 등)만 Mock으로 교체하면 통합 수준의 검증이 가능합니다.
References
Section titled “References”- 15a-unit-testing.md — unit 테스트 규칙 (명명, AAA 패턴, MTP 설정)
- 08b-error-system-domain-app.md — Domain/Application 에러 Assertion 패턴
- 08c-error-system-adapter-testing.md — Adapter 에러 Assertion 및 범용 에러 Assertion
- 01-project-structure.md — 프로젝트 구성 (HostTestFixture, 통합 테스트)
- 08-observability.md — Observability 사양 (로그 필드 정의)