문제상황
BlueprintBusiness의 테스트 코드를 작성하는 과정에서 BlueprintService에 대한 스텁을 지정했다. 그러나 해당 BlueprintService의 메서드는 반환형이 void이기 때문에 값 기반의 검증이 불가능하다.
// BlueprintBusinessTest
@Test
void 도면을_생성한다() {
// given
BlueprintRequest request = BlueprintFixture.createBlueprintRequest();
doNothing().when(blueprintService).saveBlueprint(any(Blueprint.class));
// when
blueprintBusiness.createBlueprint(request);
// then
assertThat() <- ????
}
// BlueprintService
public void saveBlueprint(Blueprint blueprint) {
validateBlueprintIsNull(blueprint);
blueprintRepository.save(blueprint);
}
고민해볼 점
Response에서 반환값 이용하기
생성된 Blueprint 객체를 Business가 Controller로 그대로 반환하여 Response Body에 표시되도록 하는 것도 좋다고 생각한다.
예를 들어, '도면 생성이 완료됐습니다. {id: 3, name: 골프장 도면}'
Repository에서 반환값이 없는 경우
update와 delete의 경우 반환값이 void이기 때문에 service에서 반환값을 지정해야 한다. 따라서 별도의 DTO를 통해 넘겨주도록 하여 이부분을 해결했다.
@Test
void 도면의_정보_중_도면이름과_작가이름을_수정한다() {
// given
final long id = 1L;
Blueprint blueprint = BlueprintFixture.createBlueprint();
BlueprintUpdateRequest request = BlueprintFixture.createBlueprintUpdateRequest();
BlueprintUpdateSuccess success = BlueprintUpdateSuccess.builder().blueprintId(id).isSuccess(true).build();
when(blueprintService.findBlueprintById(any(Long.class))).thenReturn(blueprint);
when(blueprintService.updateBlueprint(any(Blueprint.class), any(BlueprintUpdateRequest.class)))
.thenReturn(success);
// when
BlueprintUpdateSuccess blueprintUpdateSuccess = blueprintBusiness.editBlueprint(request);
// then
assertThat(blueprintUpdateSuccess.blueprintId()).isEqualTo(request.id());
assertThat(blueprintUpdateSuccess.isSuccess()).isEqualTo(true);
}
// BlueprintBusiness
@Transactional
public BlueprintUpdateSuccess editBlueprint(BlueprintUpdateRequest request) {
Blueprint blueprint = blueprintService.findBlueprintById(request.id());
return blueprintService.updateBlueprint(blueprint, request);
}
해결 방안
- 행위 기반 검증하기
assertThat을 통한 상태 기반 검증이 불가능하기 때문에 메서드가 실행된 적 있는지 확인하는 행위 기반 검증을 이용하는 것이다. - 반환값 변경하기
String을 통해 메시지를 전송하거나 Boolean을 통해 로직 처리 여부를 반환한다.
행위 기반의 문제점
assertThat을 이용한 상태 확인과 verify를 이용한 행위 확인 중 어떤 것이 더 좋은 테스트 일까? 행위 기반 검증은 사실상 알고리즘을 테스트하는 것과 같다. 이는 논리를 검증하는 것이므로 테스트 대상이 현재 코드 외에 다른 방법으로 개발하는 것이 불가능해진다. 따라서 시스템 코드가 전체적으로 경직될 수 있다. 또한 구현에 의존하기 때문에 프로덕션 코드 변경 시 테스트 코드의 변경도 수반될 가능성이 높다.
숨겨진 출력
blueprintSerivce 내부에서 아무리 로그를 남긴다고 하더라도 외부 모듈에선 이를 확인할 수 없다. 이를 숨겨진 출력
이라고 부르고 이를 해결하기 위해 반환값을 적극적으로 이용해야한다. String을 타입을 통해 '생성 완료 {id: 3}'와 같이 반환을 할 수 도 있지만, Blueprint 객체를 넘기거나 성공 여부를 알리기 위한 DTO를 통해서도 해결할 수 있다.
해결
도면 생성의 경우, 생성된 Blueprint를 반환하도록 하여 상태 기반 검증을 하도록 하였다.
@Test
void 도면을_생성한다() {
// given
BlueprintRequest request = BlueprintFixture.createBlueprintRequest();
Blueprint blueprint = Blueprint.fromRequest(request);
when(blueprintService.saveBlueprint(any(Blueprint.class)))
.thenReturn(blueprint);
// when
Blueprint savedBlueprint = blueprintBusiness.createBlueprint(request);
// then
assertThat(savedBlueprint.getBlueprintName()).isEqualTo("대한민국 마을");
}