JUnit5가 Spring을 사용할 때 테스트코드 작성을 도와주는 라이브러리인 것은 대부분의 사람들이 알것이다. 그러나 JUnit5 를 사용하더라도 단순히 사용하는 것과 왜 사용해야하는지 알고서 사용하는 것은 큰차이가 존재한다. 따라서 JUnit5를 사용하기 전에 TDD(Test Driven Development) 에 대하여 이해하고자 한다.
TDD(Test Driven Development)는 테스트 주도 개발의 약자로 개발 구현에 앞서 테스트 코드를 먼저 작성하고 테스트코드를 통과하기 위한 개발 프로세스를 가져가는 것이라고 할 수 있다. 이처럼 TDD를 설명하는 다양한 문장이 존재하는데 아래와 같다.
테스트가 개발을 이끌어간다.
개념부터 코드로 테스트를 자동화한다.
결정과 피드백 사이의 갭을 조절하는 것 => 우리가 코드를 어떻게 작성해야지라는 결정을 내리고 코드를 짜면서 에러라는 피드백을 받게 되기에 이 과정의 갭을 줄인다고 할 수 있다.
가령 우리가 작성한 백엔드 코드가 시간이 지나 레거시가 될 수 있다. 그래서 이 코드를 새로운 코드로 바꾸어야지! 하고 리팩토링을 했는데 오류가 나게 되었고 또한 수정한 곳이 너무 많아 어디서 오류가 나는지 알 수 없게 되는 결과가 나오게 된다. 이와 같은 상황이 나오기 전에 테스트 코드를 작성하게 되면 우리가 수정한 코드의 위치를 바로 알 수 있다. TDD는 우리가 코드를 작성하고 나서 배포하기 전 기능 안정성의 문지기의 역할을 하게 되는 것이다.
또한, 협업을 하는 과정에서 내가 코드를 작성하는 일보다는 다른 사람의 코드를 보고 수정하는 일이 더 많다. 이 과정에서 우리가 수정한 동료의 코드를 테스트할 때 기능에 대한 안정성을 우리가 미리 작성해놓은 Test Case가 점검하게 된다. 따라서 같이 협업하는 동료에게 설득력을 줄 수 있어 개발 과정에서 의사결정이 빨라지고 좋은 결과물을 만들 수 있게 된다.
사실 TDD 주도 개발을 하다보면 추가적인 테스트 코드를 작성해야하는 시간과 노력이 훨씬 더 많이 필요하게 된다. 단기적 성과를 목적으로 빠르게 MVP를 만들어야 하는 과정에서 좋은 코드를 작성하고 기능의 안정성을 체크하는 것보다 아이템 핵심 기능에 대한 시장의 피드백 및 테스트가 더 급할 때가 존재한다. 이 과정에서 굳이 유지보수 비용을 늘릴 필요는 없게 되는 것이다.
하지만 투입되는 노력의 총량과 유지보수의 비용이 많아지더라도 TDD를 해야하는 확실한 이유는 "기능의 안정성" 때문이다. 이 "기능의 안정성"은 개발자에게 내가 짠 코드가 맞다는 확신을 주게 된다. 따라서 개발 과정 내 피드백과 협력이 증진해 결함이 줄어들고 코드 복잡도도 떨어져 장기적으로 유지보수 비용이 덜 들어가게 된다.
테스트 주도 개발에는 크게 3가지의 종류가 존재한다.
가장 작은 단위의 테스트로 예상대로 동작하는지 확인하는 테스트이다. 보통 Method 단계에서 실행되어 A 라는 함수를 실행했을 때 B라는 결과가 나오는 정도로 테스트한다. 즉각적인 피드백이 나온다는 장점이 존재한다.
통합테스트는 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈들을 모다 이들이 의도대로 협력하는지 확인하는 테스트이다. 우리가 외부 라이브러리도 가져와서 사용하고 또한 결제 모듈 같은 외부 요소도 함께 묶어 검증을 할 때 사용한다. 단위 테스트에서 알기 어려운 버그를 찾을 수 있는 장점이 있지만 어디서 에러가 발생하는지 확인이 쉽지 않아 유지보수가 힘들다.
인수 테스트는 사용자 스토리에 맞춰 수행하는 테스트로 실제 사용자가 서비스를 써보고 비즈니스 상에서 어떤 문제점이 있는지 피드백을 받는 테스트이다. "누가, 어떤 목적으로, 무엇을 하는지"에 대한 요소가 중심으로 소프트웨어 내부 구조보다는 사용자 관점에서 테스트를 진행한다.
우리가 JUnit5를 활용하여 테스트하는 용도는 주로 "단위 테스트"이다. Repository, Service 계층을 타겟으로 주로 실행하고 추가적으로 WebMvcTest로 Controller 계층에서 일부 "통합 테스트"도 진행하게 된다.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit5는 위 3가지가 합쳐진 새로운 버전이라고 볼 수 있다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
여기서 assertEquals는 assertEquals(expected, actual) 로 기대되는 요소를 2를 넣고 실제로 실행한 코드는 actual 상에서 발생 해 기대값과 실행 결과를 비교하게 된다.
어노테이션은 JUnit5 JUnit Jupiter가 테스트 환경을 제공해주는 테스트 코드의 꽃이라고 볼 수 있다.
org.junit.jupiter.api
@Test는 이 함수가 테스트 함수임을 나타내는 것으로 다른 속성들까지 선언하지는 않는다. 단순히 선언을 함으로써 자식 요소는 오버라이딩하지 않는 이상 상속이 된다.
@Test
void 테스트() {
}
서로 다른 인자들로 여러 번의 테스트를 수행할 때 사용한다. @Test 처럼 사용이 되지만 추가적으로 적어도 이 인자들이 어디서 오는지 그 Source 를 선언할 필요가 있다.
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ValueSource에서 제공해주는 단어들을 기반으로 이 단어가 팰린드롬인지 아닌지 판별을 하게 된다.
동적으로 테스트를 해보고자 하는 경우에 사용하는 어노테이션이다. 우리가 특정 메소드를 반복적으로 수행할 경우 이 RepeatedTest를 사용하게 된다.
@RepeatedTest(10)
void 반복_테스트() {
}
반복_테스트
는 결과적으로 라이프사이클을 돌면서 10번을 수행하게 된다.
우리가 수행하는 함수의 프로세스를 DisplayName을 사용해서 설명할 수 있다.
@Test
@DisplayName("실제로 나오는 이름")
void 함수_이름() {
}
@Test, @RepeatedTest, @ParameterizedTest가 각각 실행되기 이전에 실행시켜주는 어노테이션이다.
@BeforeEach
void 이전에_실행() {
System.out.println("테스트 실행!!")
}
@Test
void test1() {
System.out.println("Test 1")
}
@Test
void test2() {
System.out.println("Test 2")
}
그러면 결과적으로 콘솔에 "테스트 실행!!" -> "Test 1" -> "테스트 실행!!" -> "Test 2"
순으로 나오게 된다.
AfterEach는 BeforeEach와 반대로 @Test 실행 이후 나오게 되는 요소이다.
@Test
void test1() {
System.out.println("Test 1")
}
@Test
void test2() {
System.out.println("Test 2")
}
@AfterEach
void 이후에_실행() {
System.out.println("테스트 실행!!")
}
그러면 결과적으로 콘솔에 "Test 1" -> "테스트 실행!!" -> "Test 2" -> "테스트 실행!!"
순으로 나오게 된다.
BeforeAll은 모든 @Test가 실행되기 전에 실행되도록 하는 어노테이션이다.
@BeforeAll
void 이전에_실행() {
System.out.println("테스트 실행!!")
}
@Test
void test1() {
System.out.println("Test 1")
}
@Test
void test2() {
System.out.println("Test 2")
}
그러면 결과적으로 콘솔에 "테스트 실행!!" -> "Test 1" -> "Test 2"
순으로 나오게 된다.
AfterAll은 모든 @Test가 실행되고 나서 사용하는 어노테이션이다.
@Test
void test1() {
System.out.println("Test 1")
}
@Test
void test2() {
System.out.println("Test 2")
}
@AfterAll
void 이후에_실행() {
System.out.println("테스트 실행!!")
}
그러면 결과적으로 콘솔에 "Test 1" -> "Test 2" -> "테스트 실행!!"
순으로 나오게 된다.
Disabled는 우리가 작성한 테스트 함수 중에 실행하지 않게 하고자하는 함수나 클래스가 있으면 쓰는 어노테이션이다.
@Test
void test1() {
System.out.println("Test 1")
}
@Disabled
void test2() {
System.out.println("Test 2")
}
@Test
void test3() {
System.out.println("Test 3")
}
그러면 결과적으로 콘솔에 Test 1 -> Test 3
이 나오게 된다.
우리가 수행하고자 하는 함수에 실행 순서를 지정할 수 있다.
@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
// perform assertions against null values
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
}
}
@TestMethodOrder(OrderAnnotation.class)
를 상단에 선언하고 @Order(번호)
로 실행 순서를 지정할 수 있다.