코드스테이츠 54일차
학습 목표
- 테스트를 왜 해야하는지 이해할 수 있다.
- 단위 테스트가 무엇인지 이해할 수 있다.
- JUnit의 기본적인 사용법을 이해할 수 있다.
- JUnit을 사용하지 않고 비즈니스 로직에 단위 테스트를 적용 할 수 있다.
- 슬라이스 테스트가 무엇인지 이해할 수 있다.
- Spring에서의 슬라이스 테스트 방법을 알 수 있다.
- Hemcrest의 기본적인 사용법을 이해할 수 있다.
- Mockito의 기본적인 사용법을 이해할 수 있다.
단위 테스트란?
테스트를 해야 되는 이유
애플리케이션을 사용할 때 발생할 수 있는 에러를 최소화 할 수 있는 방법이 있다.
바로 애플리케이션에 대한 테스팅이다.
매번 수작업으로 애플리케이션을 실행시키고, Postman을 열어서 HTTP 요청을 보낸다는 것은 아무래도 비효율적이고, 불편하다.
그리고 한가지 문제점은 애플리케이션 전체가 아니라 우리가 구현한 API 계층, 서비스 계층, 데이터 액세스 계층 중에서 하나의 계층만 테스트하고 싶은 경우도 있는데 그러기가 쉽지 않다는 것이다.
또한 비즈니스 로직에서 구현한 특정 메서드만 테스트하고 싶을 때가 있을텐데 애플리케이션 실행, Postman 툴 실행 및 요청이라는 불편한 과정을 울며 겨자먹기로 해야 되는 비효율적인 상황이 발생한다.
이 경우 Java에서는 메서드 같은 아주 작은 단위를 가지는 기능들을 테스트 할 수 있는 방법이 있다.
그리고, Spring에서는 계층별로 테스트 할 수 있는 테스트 기법 역시 지원을 해주고 있다.
- 통합 테스트
기능 테스트는 테스트를 하는 주체가 주로 개발자 이외의 제 3자인 반면에 통합 테스트는 애플리케이션을 만든 개발자 또는 개발팀이 테스트의 주체가 되는것이 일반적이다.
통합 테스트는 클라이언트 측 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 이루어지는 경우가 많다.
예를 들어, 개발자가 Controller의 API를 호출하는 테스트 코드를 작성한 후 실행하면 서비스 계층과 데이터 액세스 계층을 거쳐 DB에 실제로 접속해서 기대했던 대로 동작을 하는지 테스트 하는 것은 통합 테스트의 하나라고 볼 수 있다.
하지만 통합 테스트 역시 애플리케이션의 여러 계층이 연관되어 있으며, DB까지 연결되어 있어서 독립적인 테스트가 가능하다고 볼 수는 없기때문에 단위 테스트라고 하기에는 그 범위가 매우 크다.
- 슬라이스 테스트
슬라이스 테스트는 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트를 의미한다.
API 계층, 서비스 계층, 데이터 액세스 계층이 각각 슬라이스 테스트의 대상이 될 수 있다.
하지만 슬라이스 테스트 역시 해당 계층에서 HTTP 요청이 필요하고, 외부 서비스가 연동되기도 하며 특히나 데이터 액세스 계층의 경우 여전히 DB와 연동되어 있기 때문에 슬라이스 테스트는 단위 테스트보다는 말 그대로 계층별로 쪼개어 테스트 한다는 의미의 슬라이스 테스트라고 부른다.
슬라이스 테스트의 경우, Mock(가짜) 객체를 사용해서 계층별로 끊어서 테스트 할 수 있기 때문에 어느 정도 테스트 범위를 좁히는 것이 가능하다.
슬라이스 테스트는 단위 테스트라고 부르기에는 단위가 큰 테스트이며, 또한 애플리케이션의 일부만 테스트하기 때문에 부분 통합 테스트라고 부르기도 한다는 점을 참고하자
- 단위 테스트
서비스 계층의 경우, 애플리케이션의 핵심 로직인 비즈니스 로직을 구현하는 계층이다.
일반적으로 우리가 직접 구현하는 핵심 로직 즉, 비즈니스 로직에서 사용하는 클래스들이 독립적으로 테스트하기 가장 좋은 대상이기 때문에 단위 테스트라고 부르는 경우가 가장 많다.
그렇다면 우리가 만든 기능이 기대한 대로 빠르게 동작하는지 테스트 해야하기 위해서는 주로 무얼 테스트 할까?
바로 메서드이다.
단위 테스트 코드는 메서드 단위로 대부분 작성된다고 생각하자
DB를 사용한다면 단위 테스트라고 보기 힘든것일까?
통합 테스트나 슬라이스 테스트에서 데이터베이스와 연동된다고 해서 무조건적으로 단위 테스트라고 부르기 어렵다기 보다는 데이터베이스의 상태가 테스트 이 전과 이 후가 동일하게 유지될 수 있다면 데이터베이스가 연동된다고 해도 단위 테스트에 포함될 수 는 있다.
하지만 일반적으로 단위 테스트는 최대한 독립적인 것이 좋고, 최대한 작은 단위인 것이 더 좋다.
더 작은 단위일수록 다른 연관된 기능들을 생각할 필요도 없고, 테스트 코드 짜기도 더 단순해지고 그만큼 빠르게 테스트를 수행할 수 있기 때문이다.
단위 테스트를 해야 되는 이유
그렇다면 우리가 단위 테스트를 해야되는 이유는 무엇일까?
- 우리가 샘플 애플리케이션을 만들면서 IntelliJ IDE를 실행시키고, Postman을 열어서 HTTP 요청을 보내는 조금은 번거로운 일들을 단순화 할 수 있다.
물론 HTTP 요청을 직접 보내는 테스트도 굉장히 중요하지만 매번 이렇게 하는 것은 비생산적이다. - 우리가 구현한 코드가 여러분들이 의도한 대로 동작하는지 그 결과를 빠르게 확인할 수 있다.
- 작은 단위의 테스트로 미리미리 버그를 찾을 수 있기 때문에 애플리케이션의 덩치가 커진 상태에서 문제의 원인을 찾아내는 것보다 상대적으로 더 적은 시간 안에 문제를 찾아낼 가능성이 높다.
- 버그 리포트를 전달 받을 경우, 버그가 발생한 기능의 테스트 케이스를 돌려보면서 문제가 발생한 원인을 단계적으로 찾아 가기가 용이하다.
테스트 케이스(Test Case)란?
테스트 케이스란 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세를 의미하는데, 한마디로 메서드 등 하나의 단위를 테스트하기 위해 작성하는 테스트 코드라고 생각하자
단위 테스트를 위한 F.I.R.S.T 원칙
- Fast(빠르게)
일반적으로 작성한 테스트 케이스는 빨라야 한다는 의미이다.
자주 돌려야 문제를 빨리 찾을텐데, 너무 느려서 돌리기 힘들다면 테스트 케이스를 작성하는 의미가 퇴색될 것이다.
- Independent(독립적으로)
각각의 테스트 케이스는 독립적이어야 한다는 의미이다.
일반적으로 우리가 테스트 케이스를 작성할 때, 클래스 단위로 해당 클래스 내의 메서드 동작을 테스트 한다.
메서드는 여러개 존재할 가능성이 높을테니 테스트 클래스 안에 테스트 케이스도 하나 이상이 될 것이다.
이때, 어떤 테스트 케이스를 먼저 실행시켜도 실행되는 순서와 상관없이 정상적인 실행이 보장 되어야 한다.
예를 들어, A라는 테스트 케이스를 먼저 실행시킨 후에 다음으로 B라는 테스트 케이스를 실행시켰더니 테스트에 실패하게 된다면 테스트 케이스끼리 독립적이지 않은 것이다.
- Repeatable(반복 가능하도록)
테스트 케이스는 어떤 환경에서도 반복해서 실행이 가능해야 된다는 의미이다.
IntelliJ 같은 IDE에서 버튼을 눌러서 실행을 하든, Gradle 같은 빌드 태스크를 직접 입력해서 실행을 하든, 로컬 환경이나 서버 환경에서 실행하든 반복해서 같은 결과를 확인할 수 있어야 한다.
외부 서비스나 외부 리소스가 연동되는 경우 앞에서 언급한 원칙들을 포함해서 동일한 테스트 결과 역시 보장하지 못하기때문에 단위 테스트 시에는 외부의 서비스나 리소스의 연동을 끊어주는 것이 바람직하다.
- Self-validating(셀프 검증이 되도록)
단위 테스트는 성공 또는 실패라는 자체 검증 결과를 보여주어야 한다는 의미이다.
즉, 테스트 케이스 스스로 결과가 옳은지 그른지 판단할 수 있어야 한다는 것이다.
- Timely(시기 적절하게)
단위 테스트는 테스트 하려는 기능 구현을 하기 직전에 작성해야 한다는 의미이다.
TDD(테스트 주도 개발) 개발 방식에서는 기능 구현 전에 실패하는 테스트 케이스를 먼저 작성하는 방식을 취하지만 실제로 기능 구현도 하지 않았는데 테스트 케이스부터 먼저 작성한다는건 쉽지 않다.
다만, 기능 구현을 먼저 한다 하더라도 너무 많은 구현 코드가 작성된 상태에서 테스트 케이스를 작성하려면 오히려 테스트 케이스를 작성하는데 더 많은 시간을 들일 가능성도 있다.
구현하고자 하는 기능을 단계적으로 조금씩 업그레이드하면서 그때 그때 테스트 케이스 역시 단계적으로 업그레이드 하는 방식이 더 낫다는 사실을 기억하자
✅ Given-When-Then 표현 스타일
테스트 케이스의 가독성을 높이기 위해 given - when - then 표현 방법을 사용하는 것은 테스트 케이스를 작성하는데 유용한 방법이다.
Given
- 테스트를 위한 준비 과정을 명시할 수 있다.
- 테스트에 필요한 전제 조건들이 포함된다고 보면 된다.
- 테스트 대상에 전달되는 입력 값(테스트 데이터) 역시 Given에 포함된다.
When
- 테스트 할 동작(대상)을 지정한다.
- 단위 테스트에서는 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한두줄 정도로 작성이 끝나는 부분이다.
Then
- 테스트의 결과를 검증하는 영역이다.
- 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함된다.
Assertion(어써션)이란?
테스트 세계에서 Assertion(어써션)이라는 용어는 테스트 결과를 검증할 때 주로 사용한다.
테스트 케이스의 결과가 반드시 참(true)이어야 한다는 것을 논리적으로 표현한 것이 Assertion(어써션)인데, 한마디로 ‘예상하는 결과 값이 참(true)이길 바라는 것’이라고 이해하자
총정리
- 테스트란 어떤 대상에 대한 일정 기준을 정해놓고, 그 대상이 정해진 기준에 부합하는지 부합하지 못하는지를 검증하는 과정이다.
- 우리가 IntelliJ IDE에서 애플리케이션을 실행한 후에 애플리케이션에 Postman으로 HTTP 요청을 전송해서 기대했던 JSON 응답 결과를 확인하는 것 역시 테스트이다.
- 기능 테스트는 주로 애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지 테스트 하는 것을 의미한다.
- 통합 테스트는 클라이언트 측 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 이루어지는 경우가 많다.
- 슬라이스 테스트는 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트를 의미한다.
- 일반적으로 단위 테스트는 메서드 단위로 작성된다.
- 테스트 케이스란 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세를 의미한다.
- 단위 테스트를 위한 F.I.R.S.T 원칙
- Given-When-Then 표현 스타일
- Assertion(어써션)은 ‘예상하는 결과 값이 참(true)이길 바라는 것’을 의미한다.
핵심 포인트
- JUnit의 기본 사용법을 이해할 수 있다.
- JUnit으로 작성되지 않은 단위 테스트에 JUnit을 적용할 수 있다.
JUnit이란?
JUnit은 Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 오픈 소스 테스트 프레임워크로서 사실상 Java의 표준 테스트 프레임워크라고 볼 수 있다.
TestNG라는 JUnit의 경쟁자가 있긴하지만 JUnit은 여전히 Java 애플리케이션 테스트를 위한 핵심이다.
Spring Boot Initializr에서 Gradle 기반의 Spring Boot 프로젝트를 생성하고 오픈하면 기본적으로 ‘src/test’ 디렉토리가 만들어진다.
Spring Boot Intializr를 이용해서 프로젝트를 생성하면 기본적으로 testImplementation >'org.springframework.boot:spring-boot-starter-test' 스타터가 포함되며, JUnit도 포함이 되어있어 별다른 설정 없이 JUnit을 사용할 수 있다.
✔ JUnit을 사용한 테스트 케이스의 기본 구조
import org.junit.jupiter.api.Test;
public class JunitDefaultStructure {
// (1)
@Test
public void test1() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
// (2)
@Test
public void test2() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
// (3)
@Test
public void test3() {
// 테스트 하고자 하는 대상에 대한 테스트 로직 작성
}
}
애플리케이션에서 테스트 하고자 하는 대상(Target)이 있으면 public void test1(){…} 같은 void 타입의 메서드 하나 만들고, @Test 애너테이션을 추가해준다.
그리고 그 내부에 테스트 하고자 하는 대상 메서드에 대한 테스트 로직을 작성해주면 된다.
✔ Assertion 메서드 사용하기
JUnit에서는 Assertion과 관련된 다양한 메서드를 사용해서 테스트 대상에 대한 Assertion을 진행할 수 있다.
- assertEquals()
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HelloJUnitTest {
@DisplayName("Hello JUnit Test") // (1)
@Test
public void assertionTest() {
String expected = "Hello, JUnit";
String actual = "Hello, JUnit";
assertEquals(expected, actual); // (2)
}
}
- assertNotNull() : Null 여부 테스트
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class AssertionNotNullTest {
@DisplayName("AssertionNull() Test")
@Test
public void assertNotNullTest() {
String currencyName = getCryptoCurrency("ETH");
// (1)
assertNotNull(currencyName, "should be not null");
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit);
}
}
(1)에서 assertNotNull() 메서드를 사용하면 테스트 대상 객체가 null 이 아닌지를 테스트할 수 있다.
assertNotNull() 메서드의 첫 번째 파라미터는 테스트 대상 객체이고, 두 번째 파라미터는 테스트에 실패했을 때, 표시할 메시지이다.
- assertThrows() : 예외(Exception) 테스트
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class AssertionExceptionTest {
@DisplayName("throws NullPointerException when map.get()")
@Test
public void assertionThrowExceptionTest() {
// (1)
assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit).toUpperCase();
}
}
(1)에서 assertThrows()의 첫 번째 파라미터에는 발생이 기대되는 예외 클래스를 입력하고, 두 번째 파라미터인 람다 표현식에서는 테스트 대상 메서드를 호출하면 된다.
테스트 케이스를 실행하면 getCryptoCurrency() 메서드가 호출되고, 파라미터로 전달한 “XRP”라는 키에 해당하는 암호 화폐가 있는지 map에서 찾는다.
하지만 XRP 에 해당하는 암호 화폐는 map에 존재하지 않기 때문에 map에서 반환된 값은 null이 될 것이다.
그리고 map에서 반환된 값이 null인 상태에서 toUpperCase()를 호출해서 대문자로 변환하려고 했기 때문에 NullPointerException이 발생할 것이다.
따라서 (1)에서 NullPointerException이 발생할 것이라고 기대했기 때문에 테스트 실행 결과는 “passed”이다.
✅ 예외 타입이 다를 경우에는 “passed”일까? “failed”일까?
만약 방금 코드에서 assertThrows()의 첫 번째 파라미터로 NullPointerException.class 대신에 IllegalStateException.class 으로 입력 값을 바꾸면 어떻게 될까?
테스트 실행 결과는 “failed”입니다. 우선 기본적으로 IllegalStateException.class 과 NullPointerException.class 은 다른 타입이고, IllegalStateException.class 이 NullPointerException.class 의 상위 타입도 아니기 때문에 테스트 실행 결과는 “failed”입니다.
그렇다면 만약 NullPointerException.class 대신에 RuntimeException.class 또는 Exception.class 으로 입력 값을 바꾸면 이번에는 테스트 실행 결과가 어떻게 될까?
이 경우, 테스트 실행 결과는 “passed”이다.
NullPointerException 은 RuntimeException 을 상속하는 하위 타입이고, RuntimeException 은 Exception 을 상속하는 하위 타입이다.
이처럼 assertThrows() 를 사용해서 예외를 테스트 하기 위해서는 예외 클래스의 상속 관계를 이해한 상태에서 테스트 실행 결과를 예상해야 된다는 사실을 기억하자
✅ 테스트 케이스 실행 시, 예외가 발생한다고 전부 “failed”일까?
애플리케이션에서 예외가 발생한다면 어떤 문제가 있는 것이기 때문에 ‘실패’, ‘문제 발생’ 같은 부정적인 단어를 떠올릴 수 있겠지만 테스트 세계에서는 다르다.
테스트 케이스 실행에서 예외가 발생한다하더라도 여러분이 예외가 발생한다라고 기대하는 순간(expected) 예외가 발생하는 것은 “passed”가 되는 것이다.
‘이 로직은 테스트 해보면 예외가 발생 안할꺼야’ 라고 기대했는데, 예외가 발생하면 “failed”인 것이다.
Executable 함수형 인터페이스
assertThrows() 의 두 번째 파라미터인 람다 표현식은 JUnit에서 지원하는 Executable 함수형 인터페이스이다.
Executable 함수형 인터페이스는 void execute() throws Throwable; 메서드 하나만 정의되어 있으며 리턴값이 없다.
Java에서 지원하는 함수형 인터페이스 중에서 리턴값이 없는 Consumer에 해당된다고 생각하자
✔ 테스트 케이스 실행 전, 전처리
테스트 케이스를 실행하기 전에 어떤 객체나 값에 대한 초기화 작업 등의 전처리 과정을 해야할 경우가 많습니다. 이 경우 JUnit에서 사용할 수 있는 애너테이션이 바로 @BeforeEach와 @BeforeAll()이다.
- @BeforeEach
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
// (1)
public class BeforeEach1Test {
@BeforeEach
public void init() {
System.out.println("Pre-processing before each test case");
}
@DisplayName("@BeforeEach Test1")
@Test
public void beforeEachTest() {
}
@DisplayName("@BeforeEach Test2")
@Test
public void beforeEachTest2() {
}
}
@BeforeEach 애너테이션을 추가한 메서드는 테스트 케이스가 각각 실행될 때 마다 테스트 케이스 실행 직전에 먼저 실행되어 초기화 작업 등을 진행할 수 있다.
실행 결과를 보면, Test case 2는 “failed”이고, Test case 1은 “passed”이다.
Test case 1은 assertDoesNotThrow()로 Assertion 하기 전에 map에 “XRP”의 값을 추가했다.
그렇기 때문에 map에 “XRP”의 값이 존재하므로 예외가 발생하지 않는다.
그런데 Test case 2는 Assertion 하기 전에 map에 “XRP”를 추가하지 않는다.
따라서 Test case 2가 실행되기 전에 init() 메서드가 호출되면서 이 전에 한번 사용했던 map 객체가 다시 초기화 된다.
Test case1에서 map에 “XRP”를 추가했다 하더라도 추가한 “XRP”는 Test case2 실행 전에 init() 메서드가 다시 호출되면서 map이 초기화 되기 때문에 초기화 된 상태로 되돌아 간다.
따라서, Test case 2에서 예외가 발생하지 않는다고 기대했지만 NullpointerException이 발생하므로 테스트 실행 결과는 “failed”이다.
콘솔에 출력된 “{BTC=Bitcoin, POT=Polkadot, ETH=Ethereum, ADA=ADA}”는 Test case 2 실행 시, map의 상태를 출력한 것이다. map 안에 “XRP”는 없는 것을 확인할 수 있다.
- @BeforeAll()
@BeforeAll()은 @BeforeEach() 와 달리 클레스 레벨에서 테스트 케이스를 한꺼번에 실행 시키면 테스트 케이스가 실행되기 전에 딱 한번만 초기화 작업을 할 수 있도록 해주는 애너테이션이다.
public class BeforeAllTest {
private static Map<String, String> map;
@BeforeAll
public static void initAll() {
map = new HashMap<>();
map.put("BTC", "Bitcoin");
map.put("ETH", "Ethereum");
map.put("ADA", "ADA");
map.put("POT", "Polkadot");
map.put("XRP", "Ripple");
System.out.println("initialize Crypto Currency map");
}
@DisplayName("Test case 1")
@Test
public void beforeEachTest() {
assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
}
@DisplayName("Test case 2")
@Test
public void beforeEachTest2() {
assertDoesNotThrow(() -> getCryptoCurrency("ADA"));
}
private String getCryptoCurrency(String unit) {
return map.get(unit).toUpperCase();
}
}
@BeforeAll 애너테이션을 사용해서 map 객체를 한 번만 초기화 하기 때문에 두 개의 테스트 케이스 실행 결과는 모두 “passed”이다.
그리고, 콘솔에는 아래와 같이 “initialize Crypto Currency map”이 한번만 출력된다.
@BeforeAll 애너테이션을 추가한 메서드는 정적 메서드(static method)여야 한다는 사실을 기억하자
✔ 테스트 케이스 실행 후, 후처리
JUnit에서는 테스트 케이스 실행이 끝난 시점에 후처리 작업을 할 수 있는 @AfterEach, @AfterAll 같은 애너테이션도 지원한다.
이 애너테이션은 @BeforeEach , @BeforeAll 과 동작 방식은 같고, 호출되는 시점만 반대이다.
✔ Assumption을 이용한 조건부 테스트
JUnit 5의 Assumption 기능을 사용하면 특정 환경에만 테스트 케이스가 실행 되도록 할 수 있다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class AssumptionTest {
@DisplayName("Assumption Test")
@Test
public void assumptionTest() {
// (1)
assumeTrue(System.getProperty("os.name").startsWith("Windows"));
// assumeTrue(System.getProperty("os.name").startsWith("Linux")); // (2)
System.out.println("execute?");
assertTrue(processOnlyWindowsTask());
}
private boolean processOnlyWindowsTask() {
return true;
}
}
(1)에서 assumeTrue() 메서드는 파라미터로 입력된 값이 true이면 나머지 아래 로직들을 실행한다.
만약 테스트 케이스를 실행하는 PC의 운영체제(OS)가 윈도우즈(Windows)라면 assumeTrue() 메서드의 파라미터 값이 true가 될 것이므로 assumeTrue() 아래 나머지 로직들이 실행이 될 것이고, PC 운영체제(OS)가 윈도우즈(Windows)가 아니라면 assumeTrue() 아래 나머지 로직들이 실행되지 않을 것이다.
이처럼, assumeTrue()는 특정 OS 환경 등의 특정 조건에서 선택적인 테스트가 필요하다면 유용하게 사용할 수 있는 JUnit 5의 API이다.
JUnit으로 비즈니스 로직에 단위 테스트 적용해보기
public class StampCalculatorTestWithoutJUnit {
public static void main(String[] args) {
calculateStampCountTest(); // (1) 첫 번째 단위 테스트
calculateEarnedStampCountTest(); // (2) 두 번째 단위 테스트
}
private static void calculateStampCountTest() {
// given
int nowCount = 5;
int earned = 3;
// when
int actual = StampCalculator.calculateStampCount(5, 3);
int expected = 7;
// then
System.out.println(expected == actual);
}
private static void calculateEarnedStampCountTest() {
// given
Order order = new Order();
OrderCoffee orderCoffee1 = new OrderCoffee();
orderCoffee1.setQuantity(3);
OrderCoffee orderCoffee2 = new OrderCoffee();
orderCoffee2.setQuantity(5);
order.setOrderCoffees(List.of(orderCoffee1, orderCoffee2));
// when
int actual = StampCalculator.calculateEarnedStampCount(order);
int expected = 8;
// then
System.out.println(expected == actual);
}
}
총정리
- JUnit은 Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 오픈 소스 테스트 프레임워크이다.
- JUnit은 2022년 현재 Junit 5가 릴리즈 되어 있다.
- JUnit으로 테스트 케이스를 작성하기 위해서는 기본적으로 @Test 애너테이션을 추가해야한다.
- JUnit은 assertXXXX()로 시작하는 다양한 Assertion 메서드를 지원한다.
- JUnit은 테스트 케이스 실행 전, 후에 어떤 처리 로직을 작성할 수 있도록 @BeforeEach, @BeforeAll, @AfterEach, @AfterAll 등의 애너테이션을 지원한다.