본문으로 건너뛰기

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로 주입받는 경우
  • 캐시 만료, 재연결 타이머, 폴링 주기 등 시간 기반 로직 테스트
  • 실시간 대기 없이 시간 경과를 시뮬레이션할 때

관련 문서​