Caffeine.Testing API
개요
Caffeine.Testing은 Caffeine 드라이버 플러그인 개발자를 위한 테스트 인프라 패키지입니다. IDriverModule 구현체의 생명주기·데이터 수집·쓰기 동작을 손쉽게 검증할 수 있는 Fluent API를 제공합니다.
주요 컴포넌트
| 클래스 | 역할 |
|---|---|
DriverTestHarness | 드라이버 테스트 시나리오 빌더 및 실행기 |
EquipmentSimulatorBuilder | 가상 장비 동작 설정 (태그값, 지연, 실패율) |
FakeClock | 시간 의존 테스트용 IClock 구현체 |
DriverTestResult | 테스트 실행 결과 (Pass/Fail, 경과 시간, 메트릭) |
기본 정보
| 항목 | 값 |
|---|---|
| 패키지 | NEXCODE.Caffeine.Testing |
| 타겟 프레임워크 | net10.0 |
| 네임스페이스 | Caffeine.Testing |
| 주요 의존성 | Caffeine.Core, Microsoft.Extensions.Logging |
설치
<ItemGroup>
<!-- 테스트 프로젝트에만 참조 (프로덕션 코드에 포함하지 않음) -->
<PackageReference Include="NEXCODE.Caffeine.Testing" Version="2.1.*" />
<PackageReference Include="xunit.v3" Version="*" />
<PackageReference Include="Moq" Version="4.*" />
</ItemGroup>
DriverTestHarness
드라이버 통합 테스트를 구성하고 실행하는 핵심 클래스입니다.
Builder 메서드
| 메서드 | 설명 | 기본값 |
|---|---|---|
WithDriver<TDriver>() | 테스트 대상 드라이버 타입 등록 | - |
WithDriverFactory(Func<IDriverModule>) | 커스텀 드라이버 생성 로직 (DI 없이 수동 생성 시) | - |
WithSimulatedEquipment(Action<EquipmentSimulatorBuilder>) | 가상 장비 동작 설정 | - |
WithTimeout(TimeSpan) | 테스트 타임아웃 | 30초 |
WithClock(IClock) | 시간 제어 (FakeClock 주입) | SystemClock |
WithLogger(ILoggerFactory) | 로거 팩토리 주입 | NullLoggerFactory |
테스트 실행 메서드
| 메서드 | 설명 |
|---|---|
RunLifecycleTestAsync(ct) | 생명주기 테스트: 생성 → InitializeAsync() → DisposeAsync() |
RunDataCollectionTestAsync(ct) | 데이터 수집 테스트: Init → ReadBytesAsync() → 결과 확인 → Dispose |
RunWriteTestAsync(ct) | 쓰기 테스트: Init → WriteBytesAsync() → 예외 없음 확인 → Dispose |
DriverTestResult
| 속성/메서드 | 타입 | 설명 |
|---|---|---|
IsSuccess | bool | 테스트 통과 여부 |
TestName | string | 테스트 이름 ("Lifecycle", "DataCollection", "Write") |
Elapsed | TimeSpan | 실행 경과 시간 |
ErrorMessage | string? | 실패 시 오류 메시지 |
Exception | Exception? | 실패 시 원인 예외 |
Metrics | IReadOnlyDictionary<string, object> | 측정값 (BytesRead 등) |
Pass(name, elapsed, metrics) | 정적 팩토리 | 성공 결과 생성 |
Fail(name, elapsed, msg, ex) | 정적 팩토리 | 실패 결과 생성 |
EquipmentSimulatorBuilder
가상 장비의 응답 동작을 설정하는 Fluent Builder입니다.
메서드
| 메서드 | 설명 |
|---|---|
WithTag(string name, object value) | 태그 이름과 초기값 등록 |
WithTags(IDictionary<string, object>) | 다수 태그 일괄 등록 |
WithResponseDelay(TimeSpan) | ReadBytesAsync 호출 시 인위적 지연 추가 |
WithFailureRate(double rate) | 읽기 실패 확률 설정 (0.0 ~ 1.0) |
WithReadHandler(Func<string, Memory<byte>, Task<int>>) | 커스텀 읽기 핸들러 등록 |
FakeClock
시간 의존 테스트를 위한 IClock 구현체입니다. SetUtcNow()와 Advance()로 시간을 임의로 조작할 수 있습니다.
생성자
// 기본 초기 시간: 2025-01-01 00:00:00 UTC
var clock = new FakeClock();
// 커스텀 초기 시간 지정
var clock = new FakeClock(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
메서드 및 속성
| 멤버 | 설명 |
|---|---|
UtcNow | 현재 UTC 시간 (테스트에서 설정된 값) |
UnixMilliseconds | Unix epoch 이후 경과 밀리초 |
SetUtcNow(DateTimeOffset) | 시간을 특정 값으로 설정 |
Advance(TimeSpan) | 현재 시간을 지정한 간격만큼 전진 |
사용 예제
기본 생명주기 테스트
using Caffeine.Testing;
using Xunit;
public class MyDriverLifecycleTests
{
[Fact]
public async Task Driver_ShouldCompleteLifecycle_Successfully()
{
// Arrange
var harness = new DriverTestHarness()
.WithDriver<MyModbusDriver>()
.WithTimeout(TimeSpan.FromSeconds(10));
// Act
var result = await harness.RunLifecycleTestAsync();
// Assert
Assert.True(result.IsSuccess, result.ErrorMessage);
Assert.True(result.Elapsed < TimeSpan.FromSeconds(10));
}
}
시뮬레이터와 함께 데이터 수집 테스트
[Fact]
public async Task Driver_ShouldReadData_FromSimulatedEquipment()
{
// Arrange
var harness = new DriverTestHarness()
.WithDriver<SimulationDriver>()
.WithSimulatedEquipment(sim =>
{
sim.WithTag("Motor.Speed", 1500.0)
.WithTag("Motor.Current", 12.5)
.WithTag("Temperature", 65.3)
.WithResponseDelay(TimeSpan.FromMilliseconds(10));
})
.WithTimeout(TimeSpan.FromSeconds(5));
// Act
var result = await harness.RunDataCollectionTestAsync();
// Assert
Assert.True(result.IsSuccess, result.ErrorMessage);
Assert.True((int)result.Metrics["BytesRead"] > 0);
}
FakeClock으로 시간 의존 테스트
[Fact]
public async Task Driver_ShouldHandle_TimeBasedLogic()
{
// Arrange
var fakeClock = new FakeClock(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
var harness = new DriverTestHarness()
.WithDriver<TimeSensitiveDriver>()
.WithClock(fakeClock)
.WithTimeout(TimeSpan.FromSeconds(10));
// Act - 생명주기 테스트
var result = await harness.RunLifecycleTestAsync();
// 시간 전진으로 캐시 만료 시나리오 테스트
fakeClock.Advance(TimeSpan.FromMinutes(10));
Assert.Equal(
new DateTimeOffset(2026, 1, 1, 0, 10, 0, TimeSpan.Zero),
fakeClock.UtcNow
);
Assert.True(result.IsSuccess);
}
실패율 시뮬레이션
[Fact]
public async Task Driver_ShouldHandle_IntermittentFailures()
{
// Arrange: 20% 실패율 설정
var harness = new DriverTestHarness()
.WithDriverFactory(() => new MyDriver(retryCount: 3))
.WithSimulatedEquipment(sim =>
{
sim.WithTag("Pressure", 101.325)
.WithFailureRate(0.2); // 20% 확률로 읽기 실패
});
// Act: 여러 번 실행하여 재시도 로직 검증
var results = await Task.WhenAll(
Enumerable.Range(0, 10).Select(_ => harness.RunDataCollectionTestAsync())
);
// 재시도 로직이 있으면 전체 성공률이 높아야 함
var successRate = results.Count(r => r.IsSuccess) / (double)results.Length;
Assert.True(successRate >= 0.7, $"Success rate was {successRate:P0}");
}
커스텀 읽기 핸들러
[Fact]
public async Task Driver_ShouldProcess_CustomBinaryResponse()
{
// Arrange: 바이너리 응답을 직접 제어
var harness = new DriverTestHarness()
.WithDriver<BinaryProtocolDriver>()
.WithSimulatedEquipment(sim =>
{
sim.WithReadHandler(async (address, buffer) =>
{
// 커스텀 Modbus 응답 프레임 생성
var response = new byte[] { 0x01, 0x03, 0x02, 0x00, 0xFF };
response.CopyTo(buffer);
return response.Length;
});
});
// Act
var result = await harness.RunDataCollectionTestAsync();
Assert.True(result.IsSuccess);
Assert.Equal(5, (int)result.Metrics["BytesRead"]);
}
xUnit + 로거 통합
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
public class MyDriverTests
{
private readonly ITestOutputHelper _output;
public MyDriverTests(ITestOutputHelper output) => _output = output;
[Fact]
public async Task Driver_ShouldLog_InitializationSteps()
{
// xUnit ITestOutputHelper를 ILoggerFactory로 연결
var loggerFactory = LoggerFactory.Create(builder =>
builder.AddXUnit(_output).SetMinimumLevel(LogLevel.Debug)
);
var harness = new DriverTestHarness()
.WithDriver<VerboseDriver>()
.WithLogger(loggerFactory);
var result = await harness.RunLifecycleTestAsync();
Assert.True(result.IsSuccess);
}
}
패턴 가이드
드라이버 개발 시 권장 테스트 구조
tests/Caffeine.Drivers.MyDriver.Tests/
├── Lifecycle/
│ └── DriverLifecycleTests.cs ← RunLifecycleTestAsync
├── DataCollection/
│ └── ReadBytesTests.cs ← RunDataCollectionTestAsync
├── Write/
│ └── WriteBytesTests.cs ← RunWriteTestAsync
└── Integration/
└── FullCycleTests.cs ← 복합 시나리오
언제 FakeClock을 사용하나?
- 드라이버 내부에
IClock을 DI로 주입받는 경우 - 캐시 만료, 재연결 타이머, 폴링 주기 등 시간 기반 로직 테스트
- 실시간 대기 없이 시간 경과를 시뮬레이션할 때