목 사용 팁

1. 비관리 의존성에만 목을 사용한다. 데이터베이스는 관리 의존성이므로 목을 사용하지 않는다.

 

2. 비관리 의존성을 처리하는 코드는 컨트롤러뿐이다. 따라서 단위 테스트에서는 목을 사용하지 않는다.

 

3 시스템의 끝에서 비관리 의존성과의 상호작용을 검증한다. 이를 래핑 하거나 추상화한 인터페이스를 목으로 처리하는 것보다 회귀 방지와 리팩터링 내성이 향상된다.

 

4. 시스템 끝에 있는 클래스에 대해서는 스파이가 목보다 낫다. 검증 단계에서 코드를 재사용해 테스트 크기가 줄고 가독성이 개선된다.

 

5. 모든 비관리 의존성에 하위 호환성이 동일한 수준으로 필요한 것은 아니다. 로깅과 같이 메시지의 정확한 구조가 중요하지 않고 메시지의 존재 여부와 전달하는 정보만 검증하면, 시스템의 끝에서 비관리 의존성과의 상호 작용을 검증하라는 지침을 무시할 수 있다. 

 

6. 테스트에서 사용된 목의 수는 상관 없다. 비관리 의존성의 수에 따라 달라질 뿐이다.

 

7. 비관리 의존성에 접근하는 서드파티 라이브러리 위에 어댑터를 작성하자. 기본 타입 대신 해당 어댑터를 목으로 처리한다.

테스트 대역이란

XUnit Test Patterns의 저자인 Gerard Meszaros가 정의한 용어로, SUT가 의존하는 구성 요소의 대역을 말한다. 실행 결과를 관측(ex. 이메일 발송 메서드의 호출 여부 검증)하거나 구성 요소가 제공하는 입력값을 세팅(ex. 데이터베이스 응답값 세팅)하기 용이하게 때문에 사용한다. 빠른 테스트를 위해 프로세스 외부 의존성을 대역으로 대체하기도 한다.

 

 

스텁 : sut의 동작이 구성 요소가 반환하는 값의 영향을 받는 경우, 해당 값을 간접 입력이라고 부른다. 스텁은 sut가 의존하는 실제 구성 요소를 대체하여, 테스트가 sut의 간접 입력에 대한 제어점을 가지게 한다. 즉, 데이터베이스 데이터와 같이 sut 내부로 들어오는 입력 데이터를 모방하기 위해 사용한다.

 

더미 : null이나 임의의 문자열 같이 하드코딩된 값을 의미한다. sut가 의존하는 구성 요소 중 관심 없는 요소를 더미로 처리할 수 있다.

 

페이크 : sut가 의존하는 실제 구성 요소의 기능을 대체하기 위해 사용한다. 간접 입력이나 간접 출력을 검증하지 않기 때문에, 간단한 방식으로 구현한다. 

 

목 : sut 동작에 다른 시스템이나 애플리케이션 구성 요소에서 인지할 수 있는 작업이 포함된 경우, 해당 작업을 sut의 간접 출력이라고 부른다. 목은 sut가 실행될 때 간접 출력을 확인하기 위해 사용한다. 즉, 이메일 발송과 같이 sut 외부로 나가는 상호작용을 모방하기 위해 사용한다.

 

스파이 : 목과 동일하게 간접 출력을 확인하기 위해 사용하며, 수동으로 작성하기 때문에 직접 작성한 목이라고 부르기도 한다. 

 

목 vs 스텁

테스트 대역은 사실 목과 스텁의 두 가지 유형으로 나눌 수 있다. 보다 심층적으로 알아보자.

테스트 대역의 모든 변형은 목과 스텁의 두 가지 유형으로 나눌 수 있다.

 

목 : 외부로 나가는 상호 작용을 모방하고 검사하는 데 도움이 된다. 이러한 상호 작용은 sut가 상태를 변경하기 위한 의존성을 호출하는 것에 해당한다.

스텁 : 내부로 들어오는 상호 작용을 모방하는 데 도움이 된다. 이러한 상호 작용은 sut가 입력 데이터를 얻기 위한 의존성을 호출하는 것에 해당한다.

 

이메일 발송은 smtp 서버에 사이드 이펙트를 초래하는 상호 작용, 즉 외부로 나가는 상호 작용이다. 목은 이를 모방하는 테스트 대역에 해당한다. 데이터베이스에서 데이터를 검색하는 것은 내부로 들어오는 상호 작용이다. 사이드 이펙트를 일으키지 않으며, 해당 테스트 대역은 스텁이다.

 

다른 사람이 작성한 글에 있는 예제를 같이 살펴보자

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository; // 목을 사용해 스텁 생성
    
    @Test
    void test() {
        when(userRepository.findById(anyLong())).thenReturn(new User(1, "Test User")); // 스텁을 사용해 응답값 세팅
        
        User actual = userService.findById(1);
        assertThat(actual.getId()).isEqualTo(1);
        assertThat(actual.getName()).isEqualTo("Test User");
    }
}

 

 

UserRepository 테스트 대역은 사실 목이 아니라 스텁인 것을 알 수 있다. 왜냐하면 내부로 들어오는 상호 작용, 즉 sut(UserService)에 입력 데이터를 제공하는 호출을 모방하기 때문이다. 만약 이메일 발송을 테스트 대역으로 처리했다면 이는 외부로 나가는 상호작용이기 때문에 해당 테스트 대역은 목이다. 무엇을 사용했냐가 아니라 어떻게 사용했냐에 따라 목과 스텁으로 구분되는 게 요점이다.

 

목과 스텁의 리팩터링 내성

앞서 좋은 단위 테스트 4대 요소에서 배운 리팩터링 내성이라는 관점에서 목과 스텁을 바라보자. 목은 이메일 발송과 같이 외부로 나가는 상호작용이다. 이메일 발송은 실제 사용자가 기대하는, 즉 최종 결과물에 속하기 때문에 이를 검증해도 무방하다. 구현 로직을 변경하더라도 이메일 발송이라는 결과는 변하지 않기 때문에 리팩터링 내성에 어긋나지 않는다.

스텁은 어떨까. 데이터베이스에서 데이터를 조회하는 기능이 최종 결과물일까? 그렇지 않다. 스텁은 최종 결과를 산출해내기 위한 중간 단계에 불과하다. 즉, 스텁은 sut가 출력을 생성하도록 입력값을 제공한다. 따라서 우리는 스텁을 검증하면 안 된다. 이는 테스트가 깨지기 쉬운 안티 패턴에 해당된다.

 

잘 생각해보면 스텁 응답값을 세팅하기 위해 내부 구현 메서드를 직접적으로 의존한다는 사실을 발견할 수 있다. 이는 테스트가 리팩터링 내성이 부족하다는 의미이며, 해당 데이터베이스가 내부적으로 사용하는 경우라면 변경 가능성도 크다. 단위 테스트와 통합 테스트 각각의 상황에서 어떻게 해야 할지는 추후에 다루도록 하겠다.

 

 

+ Recent posts