-
Test DoubleDEV 2023. 12. 2. 19:33
테스트 대역 = Test Double
개발하다 보면 여러 객체들 간 의존성이 발생
→ 의존성이 단위 테스트를 어렵게 만듦
→ 진짜 객체는 느리고 자원을 많이 사용하기 때문에
→ 실제 객체를 대신할 수 있는 객체가 필요
→ 가짜객체 생성 ⇒ test double (테스트 대역)
💡 test double을 mock이라고 말하는 경우도 있지만,
mock은 좁은 의미로 test double의 한 종류, 넓게는 테스트 더블을 의미하기도 한다.대역(test double)의 종류
- stub, fake, spy, mock
- 역할을 명확히 나누는 것이 애매할 경우가 있고, 서로의 특성을 조금씩 포함
input & output
- 입력 → 논리 → 출력
- 모든 프로그램은 입력 → 출력의 흐름으로 이어지는데, 입출력의 종류에는 직접입력, 집접출력, 간접입력, 간접출력이 있다.
직접입력, 직접출력
- 공개된 인터페이스를 통해 입출력
- 인터페이스만 확인하면 어떤 입출력을 갖는지 알 수 있다
- 다루기 쉬움
간접입력, 간접출력
- 입력된 인터페이스를 통한 입출력
- 인터페이스가 모듈에 입력된 후 입력된 인터페이스를 통해서 데이터를 입력받거나 출력하는 경우
- 상대적으로 복잡
대역(test double)의 종류
- stub = 간접입력
- spy = 간접출력
- fake = 실제 동작하는 구현을 제공, (mysql 대신 h2를 이용해 구현한 가짜 대역)
- mock = 기대한 대로 상호작용하는지 행위를 검증, stub이자 spy도 된다.
Mockito
- mock 객체를 지원하는 테스트 프레임워크
- stub, spy를 쉽게 처리 가능
- 대부분의 mock 객체 프레임워크가 채택한 mocking 방식
- proxy-based mocking → mockito
- classloader remapping-based mocking → mockito
Proxy pattern
- 특정 객체로의 접근을 제어하는 대리인을 제공
- 기존 코드에 변경을 가하지 않으면서 기능을 추가
- client에서 proxy 객체를 호출하여 부가적인 코드를 수행한 뒤 참조변수를 통해 RealSubject를 호출
- 클라이언트는 proxy를 통해 객체기능을 사용
→ 서비스객체의 SRP(단일책임원칙)을 해치지 않고 proxy를 통해 책임을 추가
⇒ 접근권한, 부가기능, 리턴값 변경, 로깅, 트랜잭션...
in Mockito
프록시 클래스 생성 → 프록시 클래스 인스턴스화 → mockHandler 인스턴스에 set
Dynamic proxy
- 런타임에 특정 인터페이스들을 구현하는 클래스나 인스턴스를 만드는 기술
1. JDK Dynamic proxy
- java에서 지원하는 동적 프록시 생성
- 인터페이스를 통해서만 프록시 생성 가능
- Mockito
2. CGLIB (Code Generator Library)
- JDK Dynamic proxy 보다 상대적으로 빠름
- 인터페이스가 없을 경우 사용가능
- Spring Boot AOP
Example
mock
- 테스트하려는 대상(System Under Test)에 의존이 있는 객체를 mocking 하여 처리
@InjectMocks private SetupConfig setupConfig; @Mock private SetupServiceImpl setupService; @Mock private BatchProperties batchProperties; @Test void passBeforeTask() { //given given(setupService.findData(batchProperties.getId())) .willReturn(data); given(setupService.indexData(any(Data.class))) .willReturn(Data); .... }
stub
- 테스트 하려는 대상의 의존이 있는 객체의 인터페이스를 상속한 클래스를 stub으로 만들어 임의 값을 리턴한다.
private SetupConfiguration setupConfiguration; private StubSetupServiceImpl stubSetupService = new StubSetupServiceImpl(); private BatchProperties batchProperties = new BatchProperties(); @Test void passBeforeTask() { //given SetupConfig setupConfig = new SetupConfig(batchProperties, stubSetupService); stubSetupService.setMuffinCollection(muffinCollection); ..... }
상태검증
//when setupConfig.beforeTask(taskExecution); //then final Batch batch = setupConfig.getBatch(); assertThat(batch.getTask().getIsRunning()) .isEqualTo(true); assertThat(batch.getStream().getIsRunning()) .isEqualTo(false);
- 객체의 상태를 살펴봄으로 상태를 검증
행위검증
//when setupConfig.beforeTask(taskExecution); //verify verify(setupService, times(1)) .findData(batchProperties.getId()); verify(setupService, times(1)) .indexData(any(MuffinCollection.class));
- 특정메서드가 호출되었는지 등 행위를 검증
Mockist vs Classicist
- mock을 이용하면 대역클래스(stub, spy..)를 만들지 않아도 되니 편하다.
- 하지만 출력을 어떻게 만들어 내는지를 검증하게 되고, (행위검증) → mock
- SUT(System Under Test:테스트하려는 대상)가 결과를 만들어 낼 때 의존성과 어떻게 의사소통을 하면서 결과를 만드는지 확인하게 되면 정보숨김(information hiding)에 위배되는 경우가 발생하기도 한다.
- 정보숨김: 어려운 설계, 변경가능한 설계 결정을 모듈 안쪽에 숨기는 기법
💡 mock을 사용 → SUT 내부동작을 검증 → 내부구현의 지식을 갖게 됨
→ 지식이 테스트코드로 유출 → 테스트 코드가 구현에 의존
→ 리펙터링이 힘들어진다. → 테스트가 쉽게 깨질 수 있다.728x90'DEV' 카테고리의 다른 글
Spring batch integration test (feat.Elasticsearch) (0) 2023.12.02 K8S, DNS 간헐적 5~15초 지연 (0) 2023.12.02 Dependency Mechanism (1) 2023.12.02 slf4j, facade, TDD (1) 2023.12.02 Elasticsearch Shard & 성능 (1) 2023.12.02