테스트를 위한, 테스트 주도를 위한 첫 걸음!
빠른 실패 → 빠른 성공 → 빠른 실패2 → 빠른 성공2 …
9. 테스트 범위와 종류
- 기능 테스트 : 브라우저 - 톰캣 - DB
- 통합 테스트 : 톰캣 - DB
- 단위 테스트 : 서비스, 모델 등
대역
대역 종류 | 설명 |
스텁(Stub) | 구현을 단순한 것으로 대체한다. 테스트에 맞게 단순히 원하는 동작을 수행한다. |
가짜(Fake) | 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. |
스파이(Spy) | 호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. 스텁이기도 하다. |
모의(Mock) | 기대한 대로 상호작용하는지 행위를 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모의 객체는 스텁이자 스파이도 된다. |
WireMock
JSON/XML 응답, HTTPS 지원, 단독 실행 등 다양한 기능을 제공한다.
외부 연동 코드를 테스트할 때 유용하게 사용할 수 있다.
ref. wiremock.org
10. 테스트 코드와 유지보수
- 변수나 필드를 사용해서 기댓값 표현하지 않기
- 두 개 이상을 검증하지 않기
- 정확하게 일치하는 값으로 모의 객체 설정하지 않기
- 과도하게 구현 검증하지 않기
- 셋업을 이용해서 중복된 상황을 설정하지 않기
- 통합 테스트에서 데이터 공유 주의하기
- 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기
- 실행 환경이 다르다고 실패하지 않기
- 실행 시점이 다르다고 실패하지 않기
- 랜덤하게 실패하지 않기
- 필요하지 않은 값은 설정하지 않기
- 단위 테스트를 위한 객체 생성 보조 클래스
- 조건부로 검증하지 않기
- 통합 테스트는 필요하지 않은 범위까지 연동하지 않기
11. 마치며
- 테스트 우선과 스트레스
- 회귀 테스트
- TDD 전파하기
- 짝 코딩을 통한 전파
- 레거시 코드에 대한 테스트 추가
- TDD와 개발 시간
테스트 코드 작성 → 구현 및 테스트 통과 → 리팩터링 → 테스트 코드 작성
부록 A
조건에 따른 테스트
import org.junit.jupiter.api.condition.*; @EnabledOnOs @DisabledOnOs @EnabledOnJre() @DisabledOnJre() @EnabledIfSystemProperty() @DisabledIfSystemProperty() @EnabledIfEnvironmentVariable() @DisabledIfEnvironmentVariable()
태깅과 필터링
import org.junit.jupiter.api.Tag; @Tag()
test { useJUnitPlagtform { includeTags 'intergration' excludeTags 'slow | very-slow' } }
중첩 구성
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Nested; public class Outer { // 1 @BeforeEach void outerBefore() {} // 3 @Test void outer() {} @AfterEach void outerAfter() {} // 7 @Nested class NestedA { // 2 @BeforeEach void nestedBefore() {} // 4 @Test void nested1() {} // 5 @AfterEach void nestedAfter() {} // 6 } }
- Outer 객체 생성
- NestedA 객체 생성
- outerBefore() 메서드 실행
- nestedBefore() 메서드 실행
- nestd1() 테스트 실행
- nestedAfter() 메서드 실행
- outerAfter() 메서드 실행
테스트 메시지
//... List<Integer> ret = getResuts(); List<Integer> expexted = Arrays.asList(1, 2, 3); for (int i = 0; i < expected.size(); i++) { assertEquals(expected.get(i), ret.get(i), "ret[" + i + "]"); } //...
임시 폴더 생성
import org.junit.jupiter.api.BeforeAll;import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.nio.file.Path; class TempTest { @TempDir File tempFolder; @TempDir static tempFolderPerClazz; @BeforeAll static void setup(@TempDir File tempFolder) {} @Test void fileTest(@TempDir Path tempFolder) {} }
시간 검증
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; public class TimeoutTest { @Test @Timeout void sellp2seconds() throws InterruptedException { Thread.sleep(2000); } //.. TimeoutException }
Mockito
모의 객체 생성
private Apple mockApple = mock(Apple.class); private Banana mockBanana = mock(Banana.class); final FruitBox fakeFruitBox = mock(FruitBox.class);
스텁 설정
@Test void hasTest_mock() { final FruitBox fakeFruitBox = mock(FruitBox.class); given(fakeFruitBox.get(1)) .willReturn(new Apple("나는 사과")); assertThat(fakeFruitBox.get(1).getName()) .isEqualTo("나는 사과"); }
인자 매칭 처리
@Test void addTest_mock() { final FruitBox fakeFruitBox = mock(FruitBox.class); given(fakeFruitBox.has(any())) .willReturn(true); assertThat(fakeFruitBox.has(new Apple("나는 사과"))) .isTrue(); assertThat(fakeFruitBox.has(new Banana("나는 바나나"))) .isTrue(); assertThat(fakeFruitBox.size()) .isZero(); }
- anyInt(), anyShort(), anyLong(), anyByte(), anyInt(), anyChar(), anyDouble(), anyFloat(), anyBoolean(),
- anyString()
- any() : 임의 타입
- anyList(), anySet(), anyMap(), anyCollection()
- matches(String), matches(Pattern) : 정규식
- eq(값): 특정 값과 일치 여부
given(mockList.set(anyInt(), "123").willReturn("456")); // ❌ given(mockList.set(anyInt(), eq("123")).willReturn("456")); // ⭕️
행위 검증
@Test void sizeTest_mock() { final FruitBox fakeFruitBox = mock(FruitBox.class); given(fakeFruitBox.size()).willReturn(99); assertThat(fakeFruitBox.size()).isEqualTo(99); then(fakeFruitBox).should(only()).size(); then(fakeFruitBox).should(never()).add(any()); }
인자 캡처
@Test void addTest_mock_captor() { final FruitBox fakeFruitBox = mock(FruitBox.class); Apple apple = new Apple("애플"); fakeFruitBox.add(apple); ArgumentCaptor<Fruit> argumentCaptor = ArgumentCaptor.forClass(Apple.class); then(fakeFruitBox) .should() .add(argumentCaptor.capture()); assertThat(argumentCaptor.getValue().getName()) .isEqualTo(apple.getName()); }
JUnit 5 확장 설정
import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.exceptions.base.MockitoException; @ExtendWith(MockitoException.class) public class ClazzTest { @Mock private Clazz fakeClazz; private Clazz fakeClazz2 = mock(Clazz.class); }