
헥사고날 아키텍처, DDD 패턴을 참고하여 프로젝트를 진행했던 경험이 있다.
당시 기존의 MVC패턴에서 DDD패턴을 사용하자는 의견이 나와서 팀원들 전체가 시도 및 적용 했었는데 잘 알지 못하고 사용했던 것 같다.
아키텍처와 패턴을 완벽하게 구현하기란 참 어렵다는 것을 느꼈던 것 같다.
어쨌든 당시 DDD 장점은 "테스트 케이스에 무적이다." 라는 것이였는데 프로젝트의 일정에 쫒기던 상황이라 테스트 케이스를 작성하면서 진행하지 못했다.
프로젝트가 끝난 지도 3달 정도가 지난 지금 코테 문제 풀이에만 집중하다 보니 Spring Boot도 점점 가물가물하다.
Spring Boot의 예전 기억도 되찾을 겸 사용해 보지 못하고 넘어갔던 단위 테스트에 관해서 공부하면서 구현해 보겠다.
특정 소스코드의 모듈이 의도한 대로 동작하는지 확인하고 싶을 때 사용한다.
단위 테스트는 개발자가 테스트하고 싶은 특정 부분만 독립적으로 테스트가 가능하기 때문에 코드를 리팩토링하거나 구현할 때 문제 여부를 빠르게 판단할 수 있다.
코드를 작성할 때 테스트 코드를 돌리면서 구현한다면 문제 발생 시 즉각적으로 조치가 가능할 것이다.
Spring에서의 단위 테스트는 Spring Container에 올라와 있는 Bean을 테스트하는 것이라고 한다.
JUnit이란 자바의 단위 테스트 프레임워크 이다.
SpringBoot 2.2.0 이전에는 JUnit4가 기본으로 설정되었지만, 2.2.0버전 부터는 JUnit5가 기본으로 설정 된다고 한다.
JUnit5는 런타임 시 Java8 이상을 요구하며, Gradle 4.7 이상이어야 한다.
SpringBoot initializer에서 Spring-Web 의존성을 추가하면 자동적으로 추가 된다.

기존의 JUnit4는 하나의 jar파일로 의존성을 불러와서 다른 라이브러리를 참조해서 작동하는 방식이였는데 JUnit5 부터는 그 자체로 모듈화가 되어서 작동 된다.

테스트를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스를 정의 하며 TestEngine API를 제공한다. (테스트를 실행하는 런처 제공, TestEngine을 통해 테스트를 발견하고, 실행하고, 결과를 보고한다.)
JUnit5를 지원하는 TestEngine API 구현체 이다.
개발자가 JUnit5로 작성한 테스트의 경우는 Jupiter로 진행된다.
JUnit4와 JUnit3을 지원하는 TestEngine의 구현이며 JUnit4와 JUnit3으로 작성된 테스트의 경우 Vintage로 진행된다.

//JUnit4
@Test(expected = Exception.class)
void create() throws Exception{
//생략
}
//JUnit5
@Test
void create(){
//생략
}
메서드가 테스트용 이라는 것을 나타내는 어노테이션 이다.
이전 JUnit4와 달리 속성을 선언하지 않는다.

💡 JUnit 라이프 사이클 수행과정
@BeforeAll : JUnit 클래스가 수행되면 최초 한번 @BeforeAll 어노테이션을 선언한 메서드가 실행
@BeforeEach : @Test을 찾았다면 테스트 실행 전 @BeforeEach을 선언한 메서드가 실행
@Test : @Test를 선언한 메서드가 실행
@AfterEach : @Test 실행을 마치면 @AfterEach를 선언한 메서드가 실행
@Test를 선언한 메서드가 있는지 찾으며 존재하면 (2. 과정)을 반복수행하며, 존재하지 않는 경우 (6. 과정)을 수행
@AfterAll : 실행할 @Test가 존재하지 않는다면 @AfterAll을 선언한 메서드를 수행
테스트를 종료합니다.
@BeforeAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 딱 한 번 실행되는 메서드
JUnit4의 @BeforeClass와 유사
@AfterAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 딱 한 번 실행되는 메서드
본 어노테이션을 붙인 메서드는 해당 테스트 클래스 내 테스트 메서드를 모두 실행시킨 후 딱 한번 수행되는 메서드다.
메서드 시그니쳐는 static 으로 선언해야한다.
@BeforeEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 실행되는 메서드
JUnit4의 @Before와 유사
@AfterEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 실행되는 메서드
JUnit4의 @After와 유사
매 테스트 메서드 마다 새로은 클래스를 생성하여 실행하기 때문에 효율적이지 못하다.
그렇다면 All 과 Each의 차이는 무엇일까?
@BeforeAll은 한 번만 실행되지만 @BeforeEach 매번 실행된다.
만약 특정 테스트를 실행했을 때 기존 조건들이 영향을 받아 이후 실행되는 테스트에 영향을 끼칠 우려가 있다면 매번 조건을 초기화해 주는 Each의 형태를 사용해 주는 것이 좋을 것이며 그렇지 않다면 All을 통해서 한 번만 작동하도록 하는 것이 좋을 것이다.
테스트를 하고 싶지 않은 클래스나 메서드에 적용하는 어노테이션 이다.
JUnit4의 @Ignore와 유사한 작동을 한다.
class JUnitExample{
@Test
@Disabled("Disabled")
void disbaledTest(){
//생략
}
@Test
void test(){
//생략
}
}
해당 테스트의 목적을 쉽게 표현할 수 있도록 해주는 어노테이션 이며 공백, Emoji, 특수문자 등을 지원한다.
@DisplayName("테스트 DisplayName")
class JUnitExample{
@Test
@DisplayName("DisplayName을 테스트 하기 위한 테스트")
void test(){
//생략
}
}
특정 테스트를 반복시키고 싶을 때 적용하는 어노테이션
반복적인 요청으로 성능을 측정할 때 활용한다면 좋을 것 같다.
반복 횟수와 반복 테스트 이름을 설정할 수 있다.
class JUnitExample{
@RepeatedTest(10) //10번 반복 테스트
@DisplayName("반복 테스트")
void test(){
//생략
}
}
테스트 케이스의 수행 결과를 판별하는 메서드 이며 모든 Junit Jupiter Assertions는 static 메서드 이다.

예외 발생을 확인하는 테스트
executable의 로직이 실행하는 도중 expectedType의 에러를 발생 시키는지 확인
//JUnit4
@Test(expected = Exception.class)
void test() throws Exception{
//생략
}
기존의 JUnit4는 개발자가 직접 발생하는 예외 어노테이션에 기술하고 예외가 발생하는지 유무만 확인 할 수 있었다.
//JUnit5
@Test
void test() throws Exception{
Exception e = assertThrows(Exception.class,() -> new Test(-1));
assertDoesNotThrow(() -> System.out.println("Do Something"));
}
JUnit5의 assertThrows에서는 예외를 반환받아서 예외의 상태를 검증할 수 있다.
또한 해당 부분에서 예외가 발생하지 않았음을 검증하는 assertDoesNotThrow를 사용해서 추가적인 처리를 할 수 있게 되었다.
특정 시간 안에 실행이 완료되는지 확인
Duration : 원하는 시간
Executable : 테스트할 로직
인자로 원하는 시간과 테스트 로직을 받을 수 있다.
@Rule
public Timeout timeout = Timeout.seconds(5);
class TimeoutExample{
@Test
@DisplayName("타임 아웃 정상 통과 함수")
void timeoutNotExceeded(){
assertTimeout(ofMinutes(2), () -> Thread.sleep(10));
}
@Test
@DisplayName("타임 아웃")
void timeoutExceeded(){
assertTimeout(ofMinutes(100), () -> Thread.sleep(100));
}
}
테스트 케이스를 구성할때 더 가독성 있고 유지보수하기 쉽게 구조화 하는 방법을 의미 한다.
테스트 케이스가 더 구체적이고 이해하기 쉬워지며 각각의 단계를 분리하여 각 테스트 부분이 어떤 역할을 담당하는지 명확해진다.
Given(설정) : 테스트의 초기 상태 또는 사전 조건을 설정. 입력 데이터나 테스트가 실행될 문맥을 지정
When(동작) : 테스트되는 동작 또는 이벤트를 설명하고 테스트되는 특정 메서드나 동작을 나타낸다.
Then(검증) : "When" 섹션에서 설명한 동작으로 인해 기대되는 결과 또는 동작을 정의
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
class MyTest {
@Test
void testCode() {
// Given
ArrayList<String> list = new ArrayList<>();
// When
list.add("Apple");
list.add("Banana");
list.add("Grape");
// Then
assertEquals(3, list.size());
assertTrue(list.contains("Apple"));
assertFalse(list.isEmpty());
}
}