Spring boot를 통해 테스트 코드를 작성할 때 다른 사람들의 테스트코드를 참고하면 대부분 JUnit5와 AssertJ를 같이 사용하는 것을 볼 수 있다.
코드를 따라 작성하면 테스트는 가능하지만, 이 두개가 어떤 차이점이 있고 어떻게 다르게 사용되는지 몰랐다.
그래서 테스트코드를 제대로 사용하기 위해 이 둘의 상관관계와 문법, 주로 많이 사용하는 BDD 테스트 구조에 대해 작성 해 보겠다.
@Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll과 같은 어노테이션을 사용하여 테스트 메소드와 테스트 라이프사이클을 관리한다.| 애노테이션(Annotations) | 대상 | 설명 |
|---|---|---|
@Test | 메서드 | 테스트 메서드임을 나타낸다. |
@ParameterizedTest | 메서드 | 파라미터를 가진 테스트 메소드를 작성할 수 있다. |
@RepeatedTest | 메서드 | 반복 테스트를 위한 테스트 템플릿임을 나타낸다. |
@DisplayName | 클래스, 메서드 | 테스트 클래스 또는 테스트 메서드에 대한 표시 이름을 지정한다. |
@BeforeEach | 메서드 | 현재 클래스의 각 테스트 메서드를 실행하기 전에 실행되어야 하는 메서드임을 나타낸다. |
@AfterEach | 메서드 | 현재 클래스의 각 테스트 메서드를 실행한 후에 실행되어야 하는 메서드임을 나타낸다. |
@BeforeAll | 메서드 | 현재 클래스의 모든 테스트 메서드를 실행하기 전에 실행되어야 하는 메서드임을 나타낸다. |
@AfterAll | 메서드 | 현재 클래스의 모든 테스트 메서드를 실행한 후에 실행되어야 하는 메서드임을 나타낸다. |
@Tag | 클래스, 메서드 | 필터링 테스트를 위한 태그를 선언하는데 사용한다. |
@Disabled | 클래스, 메서드 | 해당 테스트 클래스 또는 테스트 메서드를 비활성화 하는데 사용한다. |
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // value로 설정된 숫자를 하나씩 검증한다 (총 검증횟수: 6번)
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
// value의 숫자가 홀수인지 검증
assertTrue(Numbers.isOdd(number));
}
| Code | 설명 |
|---|---|
.isNotEmpty() | 값이 비어있지 않는지 판별 |
.contains("Nice") | 파라미터를 포함하고 있는지 |
.doesNotContain("ZZZ") | 파라미터를 포함하지 않는지 판별 |
.startsWith("Hell") | 파라미터로 시작하는지 판별 |
.endsWith("u.") | 파라미터로 끝나는지 판별 |
.isPositive() | 양수인지 판별 |
.isGreaterThan(3) | 파라미터 보다 큰지 판별 |
.isLessThan(4) | 파라미터 보다 작은지 판별 |
assertThat(RandomNumberGenerator.generateRandomNumber()).isBetween(0, 9) | 랜덤 숫자가 0부터 9까지의 숫자인지 판별 |
when() 메소드에서 정적 메소드를 사용할 수 없는 이유?
JUnit 5의 Mocking 프레임워크인 Mockito의 제한 때문이다. Mockito는 객체의 실제 인스턴스를 만들지 않고 가짜 객체를 만들어 사용하기 때문에 정적 메소드나 final 메소드와 같이 변경이 불가능한 메소드의 경우 사용할 수 없다.
예제 설명 참고: 벨둥의 Static Methods With Mockito , Mockito 사용한 static method Test 예제
모의 객체(Mock) 또는 스텁(Stub)을 사용하여 메소드의 동작을 검증하고 상태를 간접적으로 확인하는 Test Double 방법을 통해 실제 객체를 대체하여 테스트 할 수 있다.
void 메소드에 대한 스텁 처리를 위해서는 doAnswer() 또는 doNothing() 등을 사용해야 한다.
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
public class MyClassTest {
@Test
@DisplayName("void 메소드를 1번 실행한다.")
void test_void_method_with_stub() {
MyClass mockMyClass = mock(MyClass.class);
// void 메소드에 대한 스텁 처리
doNothing().when(mockMyClass).voidMethod();
// void 메소드 호출
mockMyClass.voidMethod();
// void 메소드가 1번 호출되었는지 확인
verify(mockMyClass, times(1)).voidMethod();
}
}
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
public class MyClassTest {
@Test
@DisplayName("void 메소드를 1번 실행하면 상태는 완료이다.")
void test_void_method_DoAnswer() {
MyClass myObject = new MyClass();
MyDependency mockDependency = mock(MyDependency.class);
// void 메소드에 대한 스텁 처리
doAnswer(new Answer<Void>() {
public Void answer(InvocationOnMock invocation) {
// 이어서 실행할 메소드
myObject.setState("COMPLETE");
return null;
}
}).when(mockDependency).voidMethod();
myObject.setDependency(mockDependency);
// void 메소드 호출
myObject.voidMethod();
// void 메소드가 1번 호출되었는지 확인
verify(mockDependency, times(1)).voidMethod();
// AssertJ를 통해 상태 검증
assertThat(myObject.getState()).isEqualTo("COMPLETE");
}
}
위 코드는 doAnswer()를 사용하여 voidMethod에 대한 스텁 처리를 하고 있다.
doAnswer()는 Answer 인터페이스를 구현하는 익명 클래스를 사용하여 void 메소드 voidMethod()의 호출에 대한 동작을 정의한다.
익명메소드인 answer 내에서 원하는 동작을 수행하고, 필요한 경우 객체의 상태를 변경하는 등 원하는 동작을 추가하여 검증이 가능하다.
맨 처음에 테스트코드를 작성할 때는 내가 무엇을 테스트 하고 싶은건지 구체적으로 생각하는게 익숙하지 않아서 테스트를 하기 위한 테스트코드 작성 시간이 굉장히 오래걸렸다.
테스트 코드를 작성하기 위해서는 내가 무엇을,왜 테스트 해야하는지 요구사항을 명확하게 알아야 했고, 정확한 기대 결과를 검증하기 위해서 어떤 흐름으로 코드를 작성 해야 하는지 생각해야 했다.
지금은 실무에서도 기능 구현을 하면 꼭 JUnit5와 AsserJ를 통해 테스트코드를 작성하고 있는데 한번 테스트 코드를 작성 해 놓으니 유지보수할 때 많은 편리함을 느끼고 있다.
이전에는 모든 케이스를 일일히 주석으로 메모하고, 테스트를 완료하면 지우기고 다른 케이스를 진행하기를 반복했었는데 테스트 케이스를 통해 모두 기록이 되기 때문에 코드가 변경되어도 쉽게 테스트가 가능하게 되었다.
아직 Mock객체나 JWT 인증을 해야 사용 가능한 메소드에 대한 테스트코드 작성은 이해가 안되는 부분이 있어서 실무에서 만들어 볼 예정이다. (추후 블로그 업로드할지도!)