Akka.NET 테스트 패턴
이 스킬을 사용해야 하는 경우
다음과 같은 경우 이 스킬을 사용하세요:
- Akka.NET 액터의 단위 테스트 작성
- 이벤트 소싱을 사용하는 영속성 액터 테스트
- 액터 상호작용 및 메시지 흐름 검증
- 액터 감독 및 생명주기 테스트
- 액터 테스트에서 외부 의존성 모킹
- 클러스터 샤딩 동작 로컬 테스트
- 액터 상태 복구 및 영속성 검증
참조 파일
- examples.md: 모든 테스트 패턴(패턴 1~8 및 알림)에 대한 완전한 코드 샘플
- anti-patterns-and-reference.md: 안티 패턴, 전통적인 TestKit, CI/CD 통합
테스트 접근 방식 선택
Akka.Hosting.TestKit 사용 (95% 사용 사례에 권장)
사용 시기:
Microsoft.Extensions.DependencyInjection을 사용하는 현대적인 .NET 애플리케이션 구축- 프로덕션에서 Akka.Hosting을 사용하여 액터 구성
- 액터에 서비스(
IOptions,DbContext,ILogger, HTTP 클라이언트 등) 주입 필요 - ASP.NET Core, Worker Services 또는 .NET Aspire를 사용하는 애플리케이션 테스트
- 현대적인 Akka.NET 프로젝트 작업(Akka.NET v1.5 이상)
장점:
- 네이티브 의존성 주입 지원 – 테스트에서 서비스를 페이크로 재정의 가능
- 프로덕션과 구성 일관성 (동일한 확장 메서드가 테스트에서도 작동)
- 액터 로직과 인프라의 깔끔한 분리
- 액터 검색을 위한 타입 안전한 ActorRegistry
전통적인 Akka.TestKit 사용
사용 시기:
- Akka.NET 코어 라이브러리 개발에 기여
Microsoft.Extensions가 없는 환경(콘솔 앱, 레거시 시스템)- DI 없이 수동
Props생성을 사용하는 레거시 코드베이스
전통적인 TestKit 패턴은 anti-patterns-and-reference.md를 참조하세요.
핵심 원칙 (Akka.Hosting.TestKit)
Akka.Hosting.TestKit.TestKit상속 – 이는 사용자 정의 클래스가 아닌 프레임워크 기본 클래스입니다.ConfigureServices()재정의 – 실제 서비스를 페이크/목(mock)으로 교체합니다.ConfigureAkka()재정의 – 프로덕션과 동일한 확장 메서드를 사용하여 액터를 구성합니다.ActorRegistry사용 – 타입 안전하게 액터 참조를 검색합니다.- 상속보다 컴포지션 – 기본 클래스가 아닌 필드로 페이크 서비스를 사용합니다.
- 사용자 정의 기본 클래스 없음 – 상속 계층 대신 메서드 재정의를 사용합니다.
- 한 번에 하나의 액터 테스트 – 의존성에 TestProbe를 사용합니다.
- 프로덕션 패턴 일치 – 동일한 확장 메서드, 다른
AkkaExecutionMode
필수 NuGet 패키지
<ItemGroup>
<!-- 코어 테스트 프레임워크 -->
<PackageReference Include="Akka.Hosting.TestKit" Version="*" />
<!-- xUnit (또는 선호하는 테스트 프레임워크) -->
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
<!-- Assertions (권장) -->
<PackageReference Include="FluentAssertions" Version="*" />
<!-- 테스트를 위한 인메모리 영속성 -->
<PackageReference Include="Akka.Persistence.Hosting" Version="*" />
<!-- 클러스터 샤딩 테스트 시 -->
<PackageReference Include="Akka.Cluster.Hosting" Version="*" />
</ItemGroup>
중요: 테스트 프로젝트의 파일 감시자 수정
Akka.Hosting.TestKit은 실제 IHost 인스턴스를 생성하며, 기본적으로 구성 다시 로드를 위해 파일 감시자를 활성화합니다. 많은 테스트를 실행할 때 Linux에서 파일 디스크립터 한도(inotify 감시 한도)를 소진할 수 있습니다.
테스트 프로젝트에 다음을 추가하세요 – 모든 테스트 실행 전에 실행됩니다:
// TestEnvironmentInitializer.cs
using System.Runtime.CompilerServices;
namespace YourApp.Tests;
internal static class TestEnvironmentInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
// 테스트 호스트에서 구성 파일 감시 비활성화
// Linux에서 파일 디스크립터 소진(inotify 감시 한도) 방지
Environment.SetEnvironmentVariable("DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE", "false");
}
}
중요한 이유:
[ModuleInitializer]는 테스트 코드보다 먼저 자동으로 실행됩니다.- 모든
IHost인스턴스에 대해 환경 변수를 전역적으로 설정합니다. - 100개 이상의 테스트 실행 시 발생하는 모호한
inotify오류를 방지합니다. IHost를 사용하는 Aspire 통합 테스트에도 적용됩니다.
테스트 패턴 개요
각 패턴은 간략하게 설명되어 있습니다. 완전한 코드 샘플은 examples.md를 참조하세요.
패턴 1: 기본 액터 테스트
기본 패턴입니다. ConfigureServices()를 재정의하여 페이크를 주입하고, ConfigureAkka()를 재정의하여 프로덕션과 동일한 확장 메서드로 액터를 등록합니다.
public class OrderActorTests : TestKit
{
private readonly FakeOrderRepository _fakeRepository = new();
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<IOrderRepository>(_fakeRepository);
}
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
builder.WithInMemoryJournal().WithInMemorySnapshotStore();
builder.WithActors((system, registry, resolver) =>
{
registry.Register<OrderActor>(system.ActorOf(resolver.Props<OrderActor>(), "order-actor"));
});
}
[Fact]
public async Task CreateOrder_Success_SavesToRepository()
{
var orderActor = ActorRegistry.Get<OrderActor>();
var response = await orderActor.Ask<OrderCommandResult>(
new CreateOrder("ORDER-123", "CUST-456", 99.99m), RemainingOrDefault);
response.Status.Should().Be(CommandStatus.Success);
_fakeRepository.SaveCallCount.Should().Be(1);
}
}
패턴 2: 액터 상호작용을 위한 TestProbe
ActorRegistry에 TestProbe를 등록하여 의존성 액터 대신 사용합니다. ExpectMsgAsync<T>()를 사용하여 메시지가 전송되었는지 확인합니다.
패턴 3: 자동 응답 TestProbe
테스트 대상 액터가 Ask를 사용하여 의존성과 통신하는 경우, 프로브에 메시지를 전달하고 응답하여 타임아웃을 방지하는 자동 응답 액터를 생성합니다.
패턴 4: 영속성 액터 테스트
WithInMemoryJournal()와 WithInMemorySnapshotStore()를 사용합니다. PoisonPill로 액터를 종료하고 쿼리하여 저널에서 복구를 강제하는 방식으로 복구를 테스트합니다.
패턴 5: 프로덕션 구성 재사용
프로덕션 확장 메서드를 항상 재사용하고 HOCON 구성을 복제하지 마세요. 이렇게 하면 테스트가 프로덕션과 정확히 동일한 구성을 사용하게 됩니다.
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
builder
.AddDraftSerializer() // 프로덕션과 동일
.AddOrderDomainActors(AkkaExecutionMode.LocalTest) // 동일하지만 로컬 모드
.WithInMemoryJournal().WithInMemorySnapshotStore(); // 테스트별 재정의
}
패턴 6: 로컬에서 클러스터 샤딩
실제 클러스터 없이 샤딩 동작을 테스트하려면 GenericChildPerEntityParent와 함께 AkkaExecutionMode.LocalTest를 사용합니다. 동일한 확장 메서드, 다른 모드입니다.
패턴 7: 비동기 작업을 위한 AwaitAssertAsync
액터가 비동기 작업을 수행할 때 AwaitAssertAsync를 사용합니다. 어설션이 통과하거나 타임아웃이 발생할 때까지 재시도하여 불안정한 테스트를 방지합니다.
await AwaitAssertAsync(() =>
{
_fakeReadModelService.SyncCallCount.Should().BeGreaterOrEqualTo(1);
}, TimeSpan.FromSeconds(3));
패턴 8: 시나리오 기반 통합 테스트
여러 액터와 상태 전환을 포함하는 완전한 비즈니스 워크플로를 종단 간 테스트합니다. 모든 도메인 액터를 등록하고 각 단계에서 상태를 확인합니다.
일반 패턴 요약
| 패턴 | 사용 사례 |
|---|---|
| 기본 액터 테스트 | 서비스가 주입된 단일 액터 |
| TestProbe | 액터가 의존성에 메시지를 보내는지 확인 |
| 자동 응답 | 테스트 시 Ask 타임아웃 방지 |
| 영속성 액터 | 이벤트 소싱 및 복구 테스트 |
| 클러스터 샤딩 | 로컬에서 샤딩 동작 테스트 |
| AwaitAssertAsync | 액터의 비동기 작업 처리 |
| 시나리오 테스트 | 종단 간 비즈니스 워크플로 |
모범 사례
- 액터당 하나의 테스트 클래스 – 테스트를 집중적으로 유지
- ConfigureServices/ConfigureAkka 재정의 – 기본 클래스를 만들지 마세요
- 목 대신 페이크 사용 – 더 간단하고 유지 관리가 용이함
- 한 번에 하나의 액터 테스트 – 의존성에 TestProbe 사용
- 프로덕션 패턴 일치 – 동일한 확장 메서드, 다른
AkkaExecutionMode - 비동기에는 AwaitAssertAsync 사용 – 불안정한 테스트 방지
- 복구 테스트 – 액터를 종료하고 다시 시작하여 영속성 확인
- 워크플로 시나리오 테스트 – 완전한 비즈니스 흐름을 종단 간 테스트
- 테스트를 빠르게 유지 – 인메모리 영속성, 실제 데이터베이스 없음
- 의미 있는 이름 사용 –
Scenario_FirstTimePurchase_SuccessfulPayment
디버깅 팁
- 디버그 로깅 활성화 – TestKit 생성자에
LogLevel.Debug전달 - ITestOutputHelper 사용 – 테스트 출력에서 액터 시스템 로그 확인
- TestProbe 검사 –
probe.Messages를 확인하여 전송된 메시지 확인 - 액터 상태 쿼리 – 디버깅을 위해 상태 쿼리 메시지 추가
- 로깅과 함께 AwaitAssertAsync 사용 – 어설션이 실패하는 이유 확인
- ActorRegistry 확인 – 액터가 올바르게 등록되었는지 확인
// 디버그 로깅이 있는 생성자
public OrderActorTests(ITestOutputHelper output)
: base(output: output, logLevel: LogLevel.Debug)
{
}