테스트
테스트의 장점
- 회귀 방지: 기존코드가 변경에도 잘 실행되는지 확인가능
- 잠재적 오류의 발견 가능성이 높음
- 테스트를 통과한 코드만 운영코드에 배포
테스트 종류
- 인수테스트: 최종 환경과 유사한 상태로 테스트
- E2E테스트: End To End Test 처음부터 끝까지 테스트
- 통합테스트: 여러 요소를 복합적으로 테스트
- 단위테스트: 필요한 단위로 쪼개서 테스트
테스트 대역
모든 유형의 비운영용 가짜 의존성을 포괄하는 용어,
스턴트 대역이라는 개념에서 시작
관리할 수 없는 것들에 주로 Mock 사용
단위 테스트
DB 포함해서 단위테스트를 작성할 때 신경쓸 부분
테스트간에 데이터가 공유되어서는 안됩니다!
- 테스트간 독립적으로 수행되고 서로의 영향을 받지 않도록 설정하는 것이 핵심!
텍스트 픽스처:
- setup, beforeeach 등으로 테스트에서 사용할 픽스처를 만들어서 사용
- 공유 될 수 있기에 내부에서만 사용할 수 있도록 처리
private User createUser() { .... }
의존성을 대체하지 않는 경우에 발생할 수 있는 이슈
- 테스트 대상의 문제가 아닌 이유로 테스트를 통과하지 못할 수 있다.
--> 텍스트 픽스처로 해결 가능
통합테스트
- 단위테스트 != 통합테스트
- 외부 의존성을 포함한 컨트롤러 영역
- 통합테스트: 하나의 단위를 초과하여 검증하는 테스트, 의존성을 갖는 테스트 (= 이메일, 데이터베이스 등)
- 관리하지 못하는 의존성에 대해서 테스트 대역을 적극적으로 활용
- 단위들이 합쳐서 발생하는 예외들을 테스트
- 상호작용이 필요한 영역
E2E테스트
- 배포된 API 를 통해서 테스트를 진행
- 처음부터 끝까지 모든 의존성을 포함한 테스트
- QA로 수행하는 테스트를 E2E 테스트라고 부를 수 있음
- 통합테스트는 일부 의존성을 포함하여 진행
- E2E테스트는 모든 의존성을 포함하기 때문에 통합테스트의 한 부분으로 볼 수 있음
테스트 구성
준비 given
실행 when
검증 then
준비
- 테스트 대상: System Under Test
- 협력자: SUT를 테스트하기 위해 필요한 요소
SUT의 메소드는 MUT
실행
필요한 만큼만 실행
검증
검증이 필요한 만큼만 검증
안티패턴
1. 단위테스트인 경우 여러 준비, 실행, 검증 사용하지 않기 --> 통합테스트
2. if 조건문으로 분기된 테스트
테스트를 분리하는 것이 좋습니다.
유사한 형태가 많다면, 매개변수화된 테스트를 사용해보는 것도 좋습니다.
3. 일반적인 비즈니스 로직에 대해 두 줄 이상으로 실행될 경우
sut.purchase
sut.decreaseQuantity
설계부분 다시 한번 생각해 볼만합니다.
4. 테스트에 외부적인 요소로 인해 결과에 영향을 주지 않기
5. 외부의존성에 의해 고통받는 경우
예측할 수 있는 테스트, 에러 관련 테스트 다 작성해보는 것
동시성 문제 테스트 -> 의존성 문제가 있음 -> 통합적으로 실제 DB 활용할 수 있습니다.
TDD
TDD 흐름
--> 테스트 --> 코딩 --> 리팩토링 -->
레드(실패) - 그린(성공) - 리팩터 로 부르기도 함
구현
쉬운 것부터 테스트
- 구현하기 쉬운 테스트부터 시작하기
- 예외 상황을 먼저 테스트 하기
테스트할 목록 정리
구현이 쉬운 테스트나 예외적인 테스트
테스트 과정에서 새로운 테스트 사례 발견시 그 사례를 목록에 추가해 놓치지 않도록 함
테스트 코드를 만들고 이를 통과시키고 리팩토링하고 다시 다음 테스트 코드를 만들고 통과시키는 과정 반복..
시작이 안 될 때는 단언부터 고민
검증하는 코드부터 작성하기
assertThat..
구현이 막히면
코드를 지우고 다시 시작
- 쉬운 테스트 , 예외적인 테스트
- 완급 조절
테스트 대역
테스트를 작성하다 보면 외부 요인이 필요한 시점이 있습니다. (File, DB, 결제, 외부 HTTP 등)
외부 요인은 테스트 작성을 어렵게 만들 뿐만 아니라 테스트 결과도 예측할 수 없게 만듭니다.
테스트 대상에서 의존하는 요인 때문에 테스트가 어려울 때는 대역(test double)을 써서 테스트를 진행할 수 있습니다.
방법 1
실제 PG 사용
불필요한 결제 내역이 쌓일 수 있습니다. (지양)
방법 2
테스트 결제를 진행할 수 있는 테스트용 계정 활용
PG 사에서 의존
방법 3
테스트 대역 사용, 구현체 대신 인터페이스 사용하는 것이 좋습니다.
[stub, spy, mock, dummy, fake]
stub
미리 준비해놓은 데이터를 가져오도록 작성
내부 상호작용
boolean existsByTitle() { return true; }
spy
관리하지 못하는 의존성에 대한 응답 데이터를 반환하도록 작성
응답 데이터를 직접 작성하여 기록한다고 생각하면 좋습니다.
외부 상호작용, 호출의 관점
boolean sendTalk() {
return true;
}
이후 실제로 해당 메소드를 따로 테스트
mock
의존성에 대한 상호작용 검증
spy와 같이 관리할 수 없는 의존성에 대해서 사용
spy의 역할을 Mock을 사용해서 대역을 만들 수 있습니다.
dummy
SUT를 동작하게 하기 위한하드코딩 등의 값
Fake
인메모리로 저장하는 FakeRepository로 만들어 사용
Map<String, Object> map = new HashMap<>();
제어하기 힘든 외부 상황이 존재할 때, 의존을 도출하고 이를 대역으로 대신할 수 있다.
- 제어하기 힘든 외부 상황을 별도 타입으로 분리
- 테스트 코드는 별도로 분리한 타입의 대역을 생성
- 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
- 대역을 이용해서 상황 구성
ex) StubCardValidator , Spy...
테스트 가능한 설계
- 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
public void setFilePath(String filePath)...
- 생성자나 세터를 통해 의존 대상 주입 받기
public PayService(PayDao payDao)
public void setPayDao(PayDao payDao)
- 시간이나 임의 값 생성 기능 분리
class Times {
public LocalDate today() {
return LocalDate.now();
}
class Loader {
public int load() {
LocalDate date = times.today();
외부 주입을 받는 것으로 분리 --> 테스트시 Mock 을 통해 임의의 시간대 / 임의 값으로 설정하여 테스트가 가능
given(times.today()).willReturn(LocalDate.of(2020.1.1));
- 외부 라이브러리는 연동하기 위한 타입(클래스)를 따로 만듭니다.
XUtil이 있다면 해당 외부객체를 다루는 XService 타입을 만들어 비즈니스 로직 구현
해당 타입을 대역으로 테스트
HttpClient 와 같이 외부 API를 가져 올 때
WireMockServer 활용
JSON/XML 응답, HTTP지원, 단독 실행 등 다양한 기능을 제공하므로 외부 연동 코드를 테스트할 때 유용하게 사용할 수 있습니다.
RestTemplate 활용
스프링부트가 테스트 목적으로 제공하는 것으로서 내장 서버에 연결
테스트 코드와 유지보수
- 변수나 필드를 사용해서 기댓값 표현하지 않기
assertEquals(1, .. ) 이런식으로 실제 값으로 표현
- 두 개 이상을 검증하지 않기
- 정확하게 일치하는 값으로 모의 객체 설정하지 않기
문자열일 경우 anyString(), Long 이면 anyLong() 등 범용적인 값을 사용
- 과도하게 구현 검증하지 않기
내부 구현보다 실행 결과를 검증
- 셋업에서 중복된 상황을 설정하지 않기
- 실행 시점이 다르다고 실패하지 않기
public boolean isExpired() --> public boolean passedExpiryDate(LocalDateTime time)
- 랜덤하게 실패하지 않기
- 랜덤 값을 생성하는 클래스로 분리하여 생성 가능
public Game(GameNumGen gen) {
nums = gen.generate();
}
- 필요하지 않은 값은 설정하지 않기
- 동일 ID 검증하는 테스트에서 이름이나, 이메일 값은 필요하지 않습니다.
- 단위테스트를 위한 객체 생성 보조 클래스
- 팩토리 메서드 생성
- 통합테스트는 필요하지 않은 범위까지 연동하지 않기
- @SpringBootTest 는 컨트롤러, 서비스 등 모든 스프링 빈을 초기화, DB 관련 외에 나머지 설정도 처리
- @JdbcTest 사용하면 DataSource, JdbcTemplate 등 DB 연동관련된 설정만 초기화
태깅과 필터링
Tag 애노테이션을 통해 테스트 포함 대상이나 제외 대상을 지정할 수 있습니다.
// build.gradle.kts
tasks.test {
useJUnitPlatform {
// 특정 태그가 있는 테스트만 실행
includeTags("fast")
// 특정 태그가 없는 테스트만 실행
excludeTags("slow")
// 복잡한 태그 표현식 사용
includeTags("fast & !slow")
}
}
- @TempDir 으로 임시 폴더 생성
- @Timeout(1) 실행시간 검증도 가능
- 임의 값 일치와 정확한 값 일치가 필요한 경우 eq() 메서드 사용
given(mockList.set(anyInt(), eq("123"))).willReturn("456");
String old = mockList.set(5,"123");
assertEquals("456", old);
JUnit5
JUnit5 주요 어노테이션
[@BeforeAll, @AfterAll, @BeforeEach, @AfterEach, @Nested]
- @BeforeAll: 모든 테스트 메서드 실행 전에 한 번 실행되는 메서드 정의
- @AfterAll: 모든 테스트 메서드 실행 후에 한 번 실행되는 메서드 정의
- 테스트 환경 설정, 리소스 초기화/해제
- @BeforeEach: 각 테스트 메서드 실행 전에 호출되는 메서드 정의
- @AfterEach: 각 테스트 메서드 실행 후에 호출되는 메서드 정의
- @Nested: 비정적 중첩 테스트 클래스
- @Nested 클래스로 비슷한 함수를 묶어서 사용할 수 있음
public class DisplayNameTest {
@Nested
class testA {
Mock
의존성에 대한 상호작용 검증할 때, 가짜 객체를 만들어 사용하는데 이것을 Mock이라고 합니다.
@Mock
- Mockito 프레임워크에서 제공하는 어노테이션
- Mock 객체를 생성할 때 사용
@InjectMocks
테스트 대상 클래스의 인스턴스를 생성하고, 해당 인스턴스에 @Mock 또는 @Spy로 선언된 의존성 객체를 자동으로 주입
이를 통해 테스트 코드를 간결하고 가독성있게 작성할 수 있습니다.
매개변수화된 테스트
-
- 매개변수화된 테스트를 작성할 테스트 클래스를 생성합니다.
- 테스트 메서드에 @ParameterizedTest 애노테이션을 적용합니다.
- 매개변수 소스를 지정하는 애노테이션을 사용합니다. 예를 들어:
- @ValueSource: 단일 값 배열 제공
- @CsvSource: 쉼표로 구분된 값 제공
- @MethodSource: 메서드에서 매개변수 제공
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class MyServiceTest { @ParameterizedTest @ValueSource(strings = {"hello", "world", "junit5"}) void testMyService(String input) { MyService service = new MyService(); assertNotNull(service.processInput(input)); } }
- 매개변수 소스 지정
- 위의 예시에서는 @ValueSource를 사용하여 문자열 배열을 매개변수로 전달했습니다.
- 다른 방법으로는 @CsvSource를 사용하여 쉼표로 구분된 값을 전달할 수 있습니다.
@ParameterizedTest @CsvSource({"hello,true", "world,false", "junit5,true"}) void testMyService(String input, boolean expected) { MyService service = new MyService(); assertEquals(expected, service.processInput(input)); }
- @MethodSource를 사용하면 별도의 메서드에서 매개변수를 제공할 수 있습니다.
@ParameterizedTest @MethodSource("provideStringsForTest") void testMyService(String input) { MyService service = new MyService(); assertNotNull(service.processInput(input)); } static Stream<String> provideStringsForTest() { return Stream.of("hello", "world", "junit5"); }
참고: 테스트 주도 개발 시작하기 - 최범균 저
'Back-end > 테스트' 카테고리의 다른 글
Spring Boot Rest APIs Test (0) | 2022.11.14 |
---|---|
Spring Boot MVC 서비스 테스트 (0) | 2022.11.09 |
Spring Boot MVC Controller Test (컨트롤러 테스트) (0) | 2022.11.05 |
Spring Boot MVC 데이터베이스 통합 테스트 @Sql (0) | 2022.10.29 |
Spring Boot Unit Testing - Mocking with Mockito - @MockBean, ReflectionTestUtils (0) | 2022.10.21 |
댓글