목차
테스트 대역 (Test Double)
실체 객체 대신 테스트 목적으로 사용되는 모든 종류의 가상 객체
단위 테스트나 통합 테스트 등 다양한 테스트 시나리오에서 중요한 역할을 하며, 실제 의존성이나 환경에 의존하지 않고 코드를 검증할 수 있도록 돕는다.
주로 Dummy, Stub, Spy, Mock, Fake를 사용하며, 특히 Mock과 Stub의 차이를 인지할 필요가 있다.
Dummy
형식적인 목적을 위해 사용되는 객체로, 실제 데이터나 로직은 담고 있지 않은 단순한 객체이다.
이는 곧 파라미터를 채우는 용도, 인터페이스 요구 사항 충족 용도로 사용된다.
UserService 클래스가 있다고 가정해보자.
해당 클래스는 login 기능이 있으며, UserDto를 파라미터로 전달받는다.
이 때, login 기능을 검증하기 위해 프로덕션 수준의 UserDto를 구현할 필요가 있을까?
해당 테스트의 목적은 login 기능을 검증하는 것이다. 따라서 UserDto라는 클래스의 기본 동작을 구현하지 않거나, 단순히 예외를 발생시키거나 null을 반환하는 등의 최소한의 기능만을 갖추고 있는 상태로 구현한다.
이를 통해, login이 동작하기 위한 파라미터에 Dummy를 채움으로서, 테스트를 수행할 수 있다.
@Test
void testLogin() {
UserService userService = new UserService();
UserDto userDto = new UserDto();
userDto.setUsername("testuser");
userDto.setPassword("testpassword");
boolean loginResult = userService.login(userDto);
assertTrue(loginResult);
}
Dummy Data와의 차이?
프로젝트 진행시 사용했던 코드이다.
해당 코드는 @PostConstruct를 통해 해당 컴포넌트가 빈으로 등록될 시 더미데이터를 DB에 저장하는 역할을 수행한다.
더미 데이터는 주로 프론트엔드 개발자나 디자이너가 실제 데이터가 아닌 임시 데이터를 사용하여 레이아웃을 설계하거나 시각적인 표현을 확인하는 등의 목적으로 사용된다.
백엔드 개발자가 이를 통해 기능을 검증한다면 넓은 의미의 테스트 대역이 될 수 있겠지만, 테스트 목적보단 단순히 데이터가 필요한 경우가 대부분이다.
본질적으로 달성하려는 목표가 다름을 인지해야 한다.
Stub
호출에 따라 대체 구현이나 미리 지정된 값(dummy)을 제공하며, 테스트를 위해 프로그래밍된 것 외에는 응답하지 않는 객체이다. 특정 시나리오를 테스트하거나 예외 상황을 재현하는 데 사용할 수 있다.
public class StubUserRepository implements UserRepository {
@Override
public User findById(long id) {
return new UserDto(1, "LSH");
}
}
해당 코드는, 정말로 pk로 해당하는 유저를 찾는것이 아닌, 미리 지정된 Dummy를 반환한다.
Dummy가 아닌 135, "hi"등의 값 자체를 반환할 수도 있다.
그렇다면, 어떻게 활용할 수 있을까?
findById 메서드에서, 상태 코드 3번은 User Not Found를 의미한다고 가정하자.
이 때, 파라미터와 상관없이 코드 3번을 반환하게 함으로서, 예외 상황이 생긴 시나리오를 가정할 수 있다.
또는, 사용자 수가 100명 이상일 때 발생하는 event를 테스트하기 위해, 사용자 수 조회 메서드가 무조건 150명을 반환하게 할 수도 있다.
이후에는, Stub의 상태 변화를 통해, 객체의 필드 값이나 상태가 예상한 대로 변경되었는지를 확인할 수 있다. 위의 경우엔, Stub 객체에 event가 일어났는지를 확인한다. 즉, 상태 검증을 수행한다.
주의할 점은, 일종의 하드 코딩과 같아질 수 있다. 그렇기에, 무조건 Stub가 실제 구현에서의 테스트를 수행했다고 판단할 순 없다. 이를 염두하고, 사용을 최소화 하는 것이 좋다.
Mock
예상되는 동작 및 기대값을 설정하고, 이에 따른 추가 동작을 정의하며, 그 동작이 실제로 발생했는지를 검증할 수 있는 객체이다.
다음은 Stub와 연계한 Mock의 활용 예시이다.
findById 메서드에서, 상태 코드 3번은 User Not Found를 의미한다고 약속했다.
Mock에는 상태코드 3번을 받아야만 한다고 가정한다.
when(UserRepository.findById(상태코드 3번).then(이후 동작 기술);
then에 3번 코드를 받은 이후 동작을 기술한다. 혹은 assert를 통해 검증한다.
즉, 해당 기대값이 발생했을 때의 추가 동작을 확인하며 동작 검증을 수행한다.
유의할 점은, 입력에 관심을 가질 필요는 없다. 해당 영역은 Stub의 몫이다.
그저 이후 동작이 어떻게 발생했는지 출력을 통해 테스트를 수행한다.
Fake
실제로 작동되는 구현을 가지고 있지만, 가볍게 구현되며 프로덕션엔 적합하지 않은 지름길을 사용하는 객체이다.
가장 대표적인 예시로, In-Memory 저장소를 사용하는 경우가 있다.
Spring Boot를 이용한 서버 구성시, DB를 connection하지 않으면 JdbcConnectionException 에러를 발생시킨다.
단순히 이 에러를 해결하기 위해 Mysql을 설치하고, connection을 구성하는 것은 효율적이지 못할 수 있다.
대신에, h2 database를 사용한다. (주로 Fake를 달성하기 위한 용도로 사용되지만, 그것만을 위한 DB는 아니다.)
이렇듯 지름길을 택함으로서, 부가적인 환경의 구성보단 테스트 목적에 집중할 수 있다.
Spy
어떤 기능이 어떤 목적으로 호출되었는지를 감시하며 정보를 기록하는 객체이다.
일반적으로는, Email 서비스를 구현한다 가정했을 때, sendEmail 메서드가 호출될 때마다 cnt 변수를 올려 호출 횟수를 기록할 수 있을 것이다. 호출 횟수 이외에도 추가 정보를 기록할 수도 있으며, 특정 조건이 충족되면 다른 동작을 하게 확장할 수도 있다. 이 경우엔, 메서드의 호출을 가로채어 대체 구현을 반환한다는 점에서 Stub의 일부로 보는 시각도 있다.
(아무리 생각해도 나는 아닌 것 같다.)
중요한 것은, Spy는 본질적으로 정보의 감시 및 추적에 중점을 둔다는 점에서 다르다. 이를 인지하자.
느낀점
모호해서 정리하면서 너무 어려웠다. 다루는 이에 따라 개념이 약간씩 변형되기도 하고, 필요 이상으로 예제 코드가 장황하기도 했다. 그래서 최대한 짧은 코드로 이해하기 쉽게 정리하는데 초점을 뒀다.
이 글에 바라는점이 있다면, 고수 분에게 잘못된 내용을 지적받는 기회가 있었으면 한다.