특정 하드웨와의 통신 인터페이스를 테스트하는 자동화된 테스트 케이스들이 있습니다. 테스트 케이스의 주요 체크 포인트는 다음과 같습니다.
하드웨어로 전송한 명령에 대한 응답이 정상 수신되는가?
하드웨어로 전송한 명령이 정상 수행되어 상태가 정상적으로 변경되는가?
그런데 테스트 케이스에 몇 가지 문제가 있습니다.
// 명령
controller.command(jsonCommand);
// 응답 대기
Thread.sleep(1000);
// 정상 응답 확인
Assertions.assertEquals(expectedResponse, controller.getLastRawResponse());
// 상태 변경 대기
Thread.sleep(2000);
// 상태 변경 확인
LastStatus lastStatus = controller.getLastStatus();
Assertions.assertEquals(expectedStatus, lastStatus);
위의 문제점을 안고 있는 일부 테스트 케이스 그룹을 실행해 보면 테스트 완료에 약 43초의 시간이 소요됩니다.
먼저 하드웨어로 부터 응답을 수신하면 테스트 케이스로 이벤트 신호를 전달할 수 있는 인터페이스를 추가했습니다. 이를 통해 고정적으로 1초를 기다리는 것이 아니라 최대 1초를 대기하는 동안 응답이 수신되면 즉시 다음을 실행하도록 하였습니다.
// 제어 명령
controller.command(jsonCommand);
// 응답 대기
CountDownLatch responseListener = new CountDownLatch(1);
controller.registerResponseListener(expectedResponse, responseListener);
boolean responseComplete = responseListener.await(1000, TimeUnit.MILLISECONDS);
// 정상 응답 확인
Assertions.assertTrue(responseComplete);
아래는 채널 파이프라인 내에 추가한 응답 알림 핸들러입니다. 응답 수신 시 외부 리스너에게 이벤트를 전달합니다.
public class ResponseNotifier extends ChannelInboundHandlerAdapter {
// 리스너 관리 맵
private final Map<String, CountDownLatch> responseListenerMap = new HashMap<>();
// 리스터 등록 인터페이스
public void registerResponseListener(String response, CountDownLatch syncObject) {
responseListenerMap.put(response, syncObject);
}
@Override
// 수신 이벤트 핸들러
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
CountDownLatch responseListener = responseListenerMap.get(lastResponse);
if (responseListener != null) {
responseListener.countDown();
}
ctx.fireChannelRead(msg);
}
}
다음은 능동적으로 자신의 상태를 알리지 않는 하드웨어를 대신해 하드웨어 상태를 직접 폴링하는 모듈을 추가하였습니다. 그래서 이번에도 역시 고정된 시간을 대기한 후 상태 변경을 체크하는 것이 아니라 대기하는 동안 상태를 반복해서 체크하여 원하는 조건을 만족하면 즉시 다음을 실행하도록 하였습니다. 이를 위해 일정 Timeout 시간 동안 상태 체크를 성공할 때까지 반복하는 공통 모듈을 하나 만들었습니다.
@Slf4j
public class ExecuteUntilSuccess {
// 쓰레드
private final ExecutorService executor = Executors.newSingleThreadExecutor();
// 실행 취소
private final CountDownLatch cancel = new CountDownLatch(1);
// 재시도 간격
private final int retryInterval = 10;
// 실행 결과 (미래)
private Future<Boolean> future;
public Future<Boolean> begin(Supplier<Boolean> tryExecute, long timeout) {
future = executor.submit(() -> {
long runningTime = 0;
// 타임아웃 시간 동안 반복
while (runningTime < timeout) {
// 성공 시도
if (tryExecute.get()) {
return true;
}
// 취소 요청 대기 = 재시도 간격 대기
if (cancel.await(retryInterval, TimeUnit.MILLISECONDS)) {
return false;
}
// 시도 시간 누적
runningTime += retryInterval;
}
return false;
});
return future;
}
public void stop() {
cancel.countDown();
try {
future.get(retryInterval * 2, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
}
그리고 위 모듈을 활용하여 하드웨어 상태를 테스트 케이스에서 폴링하고 조건 만족시 즉시 대기를 빠져나가도록 변경하였습니다.
// 실행 서비스 생성
ExecuteUntilSuccess executeUntilSuccess = new ExecuteUntilSuccess();
// 서비스 시작
Future<Boolean> future = executeUntilSuccess.begin(() -> {
LastStatus lastStatus = controller.getLastStatus();
return expectedStatus.equals(lastStatus);
}, completeMillis);
// 결과 확인
Assertions.assertTrue(future.get(completeMillis * 2, TimeUnit.MILLISECONDS));
// 실행 서비스 종료
executeUntilSuccess.stop();
수정한 코드를 테스트 해보면 약 3초의 시간이 소요되고, 40초 가량 시간이 단축된 것을 확인할 수 있었습니다.
테스트의 검증 수준도 중요하지만 테스트의 실행 시간이 빨라야 프로덕트를 수정하고 배포하기도 쉽다는 걸 깨닫고 고쳐야 겠다고 생각하고 즐겁게 수정해 볼 수 있는 시간이었습니다.