[테스트 주도 개발 시작하기]
[테스트 주도 개발 시작하기]

[테스트 주도 개발 시작하기]

Tags
최범균
TDD
JUnit
Java
Test
Published
January 7, 2024
Author
lkdcode
 
테스트를 위한, 테스트 주도를 위한 첫 걸음!
빠른 실패 → 빠른 성공 → 빠른 실패2 → 빠른 성공2 …
 

9. 테스트 범위와 종류

notion image
  • 기능 테스트 : 브라우저 - 톰캣 - DB
  • 통합 테스트 : 톰캣 - DB
  • 단위 테스트 : 서비스, 모델 등
 

대역

대역 종류
설명
스텁(Stub)
구현을 단순한 것으로 대체한다. 테스트에 맞게 단순히 원하는 동작을 수행한다.
가짜(Fake)
제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다.
스파이(Spy)
호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. 스텁이기도 하다.
모의(Mock)
기대한 대로 상호작용하는지 행위를 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모의 객체는 스텁이자 스파이도 된다.

WireMock

JSON/XML 응답, HTTPS 지원, 단독 실행 등 다양한 기능을 제공한다. 외부 연동 코드를 테스트할 때 유용하게 사용할 수 있다. ref. wiremock.org
 

10. 테스트 코드와 유지보수

  1. 변수나 필드를 사용해서 기댓값 표현하지 않기
  1. 두 개 이상을 검증하지 않기
  1. 정확하게 일치하는 값으로 모의 객체 설정하지 않기
  1. 과도하게 구현 검증하지 않기
  1. 셋업을 이용해서 중복된 상황을 설정하지 않기
  1. 통합 테스트에서 데이터 공유 주의하기
  1. 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기
  1. 실행 환경이 다르다고 실패하지 않기
  1. 실행 시점이 다르다고 실패하지 않기
  1. 랜덤하게 실패하지 않기
  1. 필요하지 않은 값은 설정하지 않기
  1. 단위 테스트를 위한 객체 생성 보조 클래스
  1. 조건부로 검증하지 않기
  1. 통합 테스트는 필요하지 않은 범위까지 연동하지 않기

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 } }
  1. Outer 객체 생성
  1. NestedA 객체 생성
  1. outerBefore() 메서드 실행
  1. nestedBefore() 메서드 실행
  1. nestd1() 테스트 실행
  1. nestedAfter() 메서드 실행
  1. 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); }