해당 내용은 WifiObserver라는 와이파이 공유기의 접속 목록을 크롤링 하여 해당 정보를 활용하는 프로젝트를 수행하며 공부한 내용입니다.
테스트를 통해 검증할 수 있는 것은 크게 아래 2가지라고 할 수 있다.
우선 상태 검증이라는 것은 적절한 "인풋"을 제공하여 원하는 "아웃풋"이 나오는지 확인하는 검증이라 할 수 있다.
int add(int a, int b) {
return a + b;
}
만약 위의 add라는 함수를 상태 검증하려면 어떻게 해야 할까?
1과 2라는 적절한 인풋을 제공하고 그 아웃풋이 3이 나오는지 확인하면 될 것이다.
이렇게 상태 검증 테스트의 경우 내가 만든 코드가 내가 원하는 대로 동작하는지 테스트하는 것이다.
그럼, 행위 검증이란 무엇을 검증하는 것일까?
말 그대로 그대로 테스트할 객체의 메서드가 어떤 행위를 하는지 검증하는 것이다.
이 행위라는 말이 애매할 수 있으리라 생각한다.
이번 프로젝트에서 행위 검증 테스트를 작성하고 또 여러 자료를 보며 느낀 행위 검증에서의 행위는 "다른 객체로의 책임 위임 행위"라고 생각한다.
그럼 이제 상태 검증에서 예시로 들었던 add의 행위를 검증해 보자.
뭔가 이상할 것이다.
"행위라고 할 수 있는 것이 없는데?"라는 생각할 것이다.
정답이다.
모든 클래스에 상태 검증과 행위 검증을 다 수행하여야 하는 것도 할 수 있는 것도 아니다.
각 검증이 필요한 클래스가 존재한다.
지금까지 파악하였을 때는 상태 검증이 필요한 클래스의 경우는 인풋에 따라 명확한 아웃풋이 결정되는 클래스들이다.
행위 검증이 필요한 클래스는 수행하는 책임이 더 작은 책임으로 나누어지는 클래스다.
우리가 사용하는 스프링에서는 서비스 계층의 클래스들이 이에 해당한다 생각한다.
아직 모호할 수 있으니 간단한 코드와 함께 살펴보자.
@RequiredArgsConstructor
public class WifiHealthClient {
private final HealthClientQuery healthClientQuery;
private final HealthClientQueryDtoConverter healthClientQueryDtoConverter;
private final HealthStatusResponseConverter healthStatusResponseConverter;
public HealthStatusResponse query(WifiHealthRequest request) {
IptimeWifiHealthClientQueryDto queryDto = getDto(request);
return healthStatusResponseConverter.from(
healthClientQuery.execute(queryDto), request.getHost());
}
private IptimeWifiHealthClientQueryDto getDto(WifiHealthRequest request) {
return healthClientQueryDtoConverter.from(request.getURL());
}
}
WifiHealthClient는 와이파이 기기에 정상적으로 접근할 수 있는지 헬스체크 하는 책임을 가지는 서비스 클래스이다.
이 클래스의 query 메서드가 하는 일은 다음과 같다.
"와이파이 기기에 정상적으로 접근할 수 있는지 헬스체크"라는 책임을 수행하기 위해 위처럼 여러 행위로 그 책임이 나누어지는 것을 확인할 수 있을 것이다.
근데 행위를 왜 검증해야 하는 것일까?
본인은 행위를 검증해야 하는 이유를 행위 검증을 통해 해당 메서드가 과도한 책임을 가졌는지 가시적으로 확인할 수 있기 때문이라 생각한다.
그리고 이렇게 가시적으로 확인한 과도한 책임을 적절한 객체를 만들어 책임을 분리해 줌으로써 객체 지향적으로 코드를 작성할 수 있게 된다 생각한다.
즉, 행위 검증은 우리가 객체지향적인 코드를 작성하였다는 하나의 기준이 될 수 있으리라 생각한다.
그럼, 행위 검증은 어떻게 할까?
다양한 방법이 있을 수 있지만 본인이 이번에 선택한 방법은 Mock을 활용하는 방법이다.
처음에는 모든 행위를 전부 검증하는 코드를 이후에는 필요한 행위만 검증하는 코드를 통해 행위 검증이 마냥 귀찮은 것은 아니라는 것을 보여주려 한다.
우선 모든 행위를 검증하기 위해 WifiHealthClient가 가지고 있는 모든 의존성은 @Mock을 통해 Mock으로 선언하였고 이들을 @InjectMocks를 통해 WifiHealthClient로 주입하였다.
@ExtendWith(MockitoExtension.class)
class MockWifiHealthClientTest {
@InjectMocks WifiHealthClient mockWifiHealthClient;
@Mock HealthClientQuery mockHealthClientQuery;
@Mock HealthClientQueryDtoConverter healthClientQueryDtoConverter;
@Mock HealthStatusResponseConverter mockHealthStatusResponseConverter;
...
}
그리고 모든 행위에 대한 요청과 응답을 설정해 주었고 이를 확인하였다.
@Test
@DisplayName("IPTIME 공유기 헬스체크 테스트")
void queryHealthTest() {
// given
WifiHealthRequest request = Dummy.request;
WifiHealthClientQueryDto healthClientQueryDto = Dummy.healthClientQueryDto;
HealthStatusResponse healthStatusResponse = Dummy.healthStatusResponse;
doReturn(healthClientQueryDto).when(mockHealthClientQueryDtoConverter).from(anyString());
doReturn(HttpStatus.OK).when(mockHealthClientQuery).execute(any(WifiHealthClientQueryDto.class));
doReturn(healthStatusResponse)
.when(mockHealthStatusResponseConverter)
.from(any(HttpStatus.class), anyString());
// when
mockIptimeWifiHealthClientImpl.query(request);
// then
verify(mockHealthClientQueryDtoConverter, times(1)).from(anyString());
verify(mockHealthClientQuery, times(1)).execute(any(WifiHealthClientQueryDto.class));
verify(mockHealthStatusResponseConverter, times(1)).from(any(HttpStatus.class), anyString());
}
모든 행위에 대한 요청과 응답을 설정하는 것이 귀찮을 수 있는데 이는 WifiHealthClient에 주입되는 모든 객체를 모두 @Mock을 통해 Mock 객체로 선언하였기 때문이다.
그러한 수고로 WifiHealthClient의 모든 행위를 검증할 수 있고 좋은 테스트 코드라 생각된다.
하지만 효율성이 떨어지는 코드라는 점에도 동의한다.
특히 Converter 클래스들의 경우 객체 변환이라는 책임을 가지고 있기는 하지만 "이러한 책임까지 검증해야 하나?"하는 생각을 들게 한다.
그래서 이러한 작은 책임들은 Mock으로 만들지 않고 Spy로 만드는 방법을 선택하였다.
Spy는 Mock과 달리 진짜 객체지만 필요한 경우에만 Mock처럼 반환 값을 지정할 수 있는 특징을 지닌다.
이러한 특징을 활용하면 위와 같이 객체 변환이라는 책임이 작고 검증할 필요가 적은 경우에는 생략하고 그렇지 않고 책임이 큰 경우에는 선언하여 행위를 검증할 수 있다.
그럼, 우선 바뀐 객체 선언부터 확인해 보면 아래와 같다.
@ExtendWith(MockitoExtension.class)
class MockWifiHealthClientTest {
@InjectMocks IptimeWifiHealthClientImpl mockIptimeWifiHealthClientImpl;
@Mock HealthClientQuery mockHealthClientQuery;
@Spy HealthClientQueryDtoConverter mockHealthClientQueryDtoConverter;
@Spy HealthStatusResponseConverter mockHealthStatusResponseConverter;
...
}
그리고 간소화한 행위 검증 테스트는 아래와 같다.
@Test
@DisplayName("IPTIME 공유기 헬스체크 테스트")
void queryHealthTest() {
// given
WifiHealthRequest request = Dummy.request;
IptimeWifiHealthClientQueryDto healthClientQueryDto = Dummy.healthClientQueryDto;
doReturn(HttpStatus.OK).when(mockHealthClientQuery).execute(healthClientQueryDto);
// when
mockIptimeWifiHealthClientImpl.query(request);
// then
verify(mockHealthClientQuery, times(1)).execute(any(IptimeWifiHealthClientQueryDto.class));
}
이전 코드에 비해 한결 간결해진 것을 확인할 수 있다.
물론 아래와 같이 Mock 객체를 사용하는 것처럼 반환값을 지정해줄 수도 있고 Spy 객체도 몇번 실행되었는지 그 행위를 검증할 수도 있다.
// 반환값 지정
doReturn(healthClientQueryDto).when(mockHealthClientQueryDtoConverter).from(anyString());
// Spy 객체 행위 검증
verify(mockHealthClientQueryDtoConverter, times(1)).from(anyString());
verify(mockHealthStatusResponseConverter, times(1)).from(any(HttpStatus.class), anyString());