본문으로 건너뛰기

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

속성/메서드타입설명
IsSuccessbool테스트 통과 여부
TestNamestring테스트 이름 ("Lifecycle", "DataCollection", "Write")
ElapsedTimeSpan실행 경과 시간
ErrorMessagestring?실패 시 오류 메시지
ExceptionException?실패 시 원인 예외
MetricsIReadOnlyDictionary<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 시간 (테스트에서 설정된 값)
UnixMillisecondsUnix 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로 주입받는 경우
  • 캐시 만료, 재연결 타이머, 폴링 주기 등 시간 기반 로직 테스트
  • 실시간 대기 없이 시간 경과를 시뮬레이션할 때

관련 문서