개발에 있어 테스트 과정은 매우 중요합니다. 작성한 코드가 의도대로 잘 작동하고 있는지, 예상치 못한 문제가 발생할 수 있는지 확인해야 합니다. 테스트 코드가 잘 작성된 프로젝트는 유지보수도 뛰어나고 예상치 못한 에러에 걱정하지 않아도 됩니다.
스프링부트에서 테스트코드를 작성하는 방법을 알아보겠습니다.
스프링부트에서 테스트 코드는 test 디렉터리에 작성합니다.
테스트 코드 작성 방식에도 다양한 패턴이 있는데, 그중 given-when-then 패턴에 대해 알아보겠습니다. given-when-then은 TDD주도 개발과 BDD 주도 개발에서 많이 쓰이는 패턴으로 세 단계로 구분해 작성합니다.
given
: 테스트 실행 준비when
: 테스트 진행then
: 테스트 결과 검증예를 들면 다음과 같습니다.
@DisplayName("새로운 멤버를 저장한다.")
@Test
public void saveMemberTest() {
// given : 멤버를 저장하기 위한 준비 과정
final String name = "Ogu";
final int age = 20;
final Member member = new Member(name, age);
// when : 실제로 멤버를 저장
final long savedId = memberService.save(member);
// then : 멤버가 잘 추가되었는지 확인
final Member savedMember = memberService.findById(savedId).get();
assertThat(savedMember.getName()).isEqualTo(name);
assertThat(savedMember.getAge()).isEqualTo(age);
}
spring-boot-starter-test
에는 테스트를 위한 도구들이 모여있습니다.
Spring Test & Spring Boot Test
: 스프링 부트 애플리케이션을 위한 통합 테스트 지원Mockito
: 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크JSONassert
: JSON용 어설션 라이브러리JsonPath
: JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리Hamcrest
Matcher Object 라이브러리로 필터, 검색등을 위해 값을 비교할 때 좀 더 편리하게 사용JUnit은 자바 언어를 위한 단위 테스트 프로그램 입니다. 단위 테스트는 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것입니다. 보통 단위는 메서드가 됩니다. JUnit의 특징은 다음과 같습니다.
@Test
애너테이션으로 메서드를 호출할 때 마다 새 인스턴스 생성, 독립 테스트 기능JUnit을 사용해 작성한 테스트 코드 예를 봅시다.
@DisplayName("1 + 2는 3이다")
@Test
public void junitTest(){
int a = 1;
int b = 2;
int sum = 3;
Assertions.assertEquals(a + b, sum); // 값이 같은지 확인
}
JUnit은 테스트끼리 서로 영향을 주지 않도록 각 테스트를 실행할 때 마다 테스트를 위한 실행 객체를 맞ㄴ들고 테스트가 종료되면 실행 객체를 삭제합니다.
@DisplayName
- 테스트 이름 명시@Test
를 붙인 메서드는 테스트를 수행하는 메서드가 됨assertEquals()
- 첫번째 인수 : expected, 두번째 값 : actual이번에는 JUnit에서 자주 사용하는 애너테이션에 대해서 알아보겠습니다.
JUnit이 각 테스트에 대해 객체를 만들어 독립적으로 실행하는 과정을 확인해 봅시다.
import org.junit.jupiter.api.*;
@BeforeAll // 전체 테스트를 시작하기 전에 1회 실행하므로 메서드는 static으로 선언
static void beforeAll() {
System.out.println("@BeforeAll");
}
@BeforeEach // 테스트 케이스를 시작하기 전마다 실행
public void beforeEach() {
System.out.println("@BeforeEach");
}
@Test
public void test1() {
System.out.println("test1");
}
@Test
public void test2() {
System.out.println("test2");
}
@Test
public void test3() {
System.out.println("test3");
}
@AfterAll // 전체 테스트를 마치고 종료하기 전에 1회 실행하므로 메서드는 static으로 선언
static void afterAll() {
System.out.println("@AfterAll");
}
@AfterEach // 테스트 케이스를 종료하기 전마다 실행
public void afterEach() {
System.out.println("@AfterEach");
}
@BeforeAll
애너테이션
전체 테스트를 시작하기 전에 처음으로 한번만 실행합니다. 전체 테스트 실행 주기에서 한 번만 호출되기 때문에 static으로 선언합니다.
ex ) 데이터베이스 연결 및 테스트 환경 초기화
@BeforeEach
애너테이션
각 테스트 케이스를 시작하기 전에 매번 실행됩니다. 각 인스턴스에 대해 메서드를 호출하므로 static이 아니어야 합니다.
ex ) 테스트 메서드에서 사용하는 객체 및 데이터 초기화
@AfterAll
애너테이션
전체 테스트를 마치고 종료하기 전에 한번만 실행합니다. 전체 테스트 실행 주기에서 한 번만 호출되므로 static으로 선언합니다.
ex ) 데이터베이스 연결 종료
@AfterEach
애너테이션
각 테스트 케이스를 종료하기 전 매번 실행합니다. @BeforeEach 애너테이션과 마찬가지로 static이 아니어야 합니다.
ex ) 테스트 이후 특정 데이터 삭제
테스트 코드를 실행해 출력 결과를 한번 살펴봅시다.
처음에는 @BeforeAll
애너테이션으로 설정한 메서드가 실행 되고, 그 이후 테스트 케이스 개수만큼 @BeforeEach -> @Test -> @AfterEach
의 생명주기로 테스트가 진행되고 있는 것을 볼 수 있습니다. 모든 테스트 케이스가 끝나고 @AfterAll
애너테이션으로 설정한 메서드를 실행하고 종료하고 있습니다.
AssertJ는 JUnit과 함께 사용하여 검증문의 가독성을 높여주는 라이브러리입니다.
예를 들어, 아까 작성했던 문장을 보면 기댓값과 비교값이 잘 구분이 되지 않습니다.
Assertions.assertEquals(a + b, sum);
이를 AssertJ를 적용하면 다음과 같습니다.
assertThat(a+b).isEqualTo(sum);
a와 b를 더한 값이 sum과 같아야 한다는 명확한 의미로 가독성이 좋아졌습니다. AssertJ에서 제공하는 메서드들에는 다음과 같은 것들이 있습니다.
메서드 이름 | 설명 |
---|---|
isEqualTo(A) | A값과 같은지 검증 |
isNotEqualTo(A) | A값과 다른지 검증 |
contains(A) | A값을 포함하는지 검증 |
doesNotContain(A) | A값을 포함하지 않는지 검증 |
startsWith(A) | 접두사가 A인지 검증 |
endsWith(A) | 접미사가 A인지 검증 |
isEmpty() | 비어있는 값인지 검증 |
isNotEmpty() | 비어있지 않은 값인지 검증 |
isPositive() | 양수인지 검증 |
isNegative() | 음수인지 검증 |
isGreaterThan(1) | 1보다 큰 값인지 검증 |
isLessThan(1) | 1보다 작은 값인지 검증 |
테스트를 진행하고 싶은 클래스에서 Ctrl + Shift + T
를 누르면 테스트 클래스를 생성할 수 있습니다.
앞서 설명한 given-when-then 패턴을 적용해 기존의 MemberController 클래스에 대해 테스트 코드를 작성해보겠습니다.
우선 controller 테스트 코드를 작성하기 위한 토대를 마련해주겠습니다.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest // 테스트용 어플리케이션 컨텍스트 생성
@AutoConfigureMockMvc // MockMVC 생성 (컨트롤러 테스트)
class TestControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Autowired
private MemberRepository memberRepository;
@BeforeEach // 테스트 실행 전 실행하는 메서드
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
}
@AfterEach // 테스트 실행 후 실행하는 메서드
public void cleanUp() {
memberRepository.deleteAll();
}
}
@SpringBootTest
애너테이션은 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 어노테이션으로, 애플리케이션이 실행 될 때의 설정을 임의로 바꿀수도 있고 여러 단위 테스트를 하나의 통합 테스트로 수행 할 수도 있습니다.
메인 애플리케이션에 추가하는 애너테이션인 @SpringBootApplication
이 있는 클래스를 찾고, 해당 클래스에 포함되어있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 것을 만듭니다.
해당 애노테이션의 컴포넌트 스캔 범위는 Bean 전체로 애플리케이션이 실행할 당시 스캔되는 범위와 동일합니다. 그렇기에 최대한 실제와 유사한 환경에서 테스트를 할 수 있다는 장점이 있습니다.
하지만, 그 말은 애플리케이션의 모든 설정을 가져오기 때문에 애플리케이션의 범위가 넓을수록 테스트가 느려질 수 밖에 없고, 이는 단위테스트의 의미를 희석하기에 단위테스트에 적절하지는 않습니다.
@AutoConfigureMockMvc
는 MockMvc를 생성하고 자동으로 구성하는 애너테이션입니다. 어플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스 입니다. 즉 컨트롤러를 테스트 할 때 사용되는 클래스입니다.
@WebMvcTest
와 비슷하지만 가장 큰 차이점은 컨트롤러 뿐만 아니라 테스트 대상이 아닌 @Service
나 @Repository
가 붙은 객체들도 모두 메모리에 올립니다.
따라서 간단한 테스트라면 @WebMvcTest
를, MockMVC를 보다 세밀하게 제어하고, 전체 애플리케이션 구성을 로드하고 MockMVC를 사용해야 한다면 @AutoConfigureMockMvc
를 사용합니다.
이제 Controller의 로직을 테스트 하는 코드를 작성해 보겠습니다.
위에 작성한 코드 아래에 다음 코드를 추가합니다.
@DisplayName("getAllMembers: 아티클 조회에 성공한다.")
@Test
public void getAllMembers() throws Exception {
// given - 멤버 저장
final String url = "/test";
Member savedMember = memberRepository.save(new Member(1L, "홍길동"));
// when - 멤버 리스트를 조회하는 API 호출
final ResultActions result = mockMvc.perform(get(url) // perform() - 요청 전송 메서드
.accept(MediaType.APPLICATION_JSON)); // 요청을 받을 때 무슨 타입으로 응답을 받을 지 결정
// then - 응답 코드가 200 OK이고, 반환받은 값 중 0번째 요소의 id와 name을 저장된 값과 확인
result
.andExpect(status().isOk()) // ResultAction 객체는 perform()메서드로 요청해서 받은 응답 검증
.andExpect(jsonPath("$[0].id").value(savedMember.getId()))
.andExpect(jsonPath("$[0].name").value(savedMember.getName()));
}
perform()
멤서드는 요청을 전송하는 메서드 입니다. 결과로 ResultActions
객체를 반환하고, ResultActions
객체는 반환을 검증하고 확인하는 andExpect()
메서드를 제공합니다.
accept()
메서드는 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드 입니다. JSON, XML 등의 타입이 있습니다.
andExpect
메서드는 응답을 검증합니다. 해당 코드에서는 응답으로 200 OK 를 반환하므로 , 이에 해당하는 메서드인 isOk
를 사용해 확인합니다.
jsonPath("$[0].${필드명}")
은 JSON 응답값을 가져옵니다. 0번째 배열에 들어있는 객체의 id와 name 값을 가져와서 저장된 값과 같은지 확인합니다.
테스트에 잘 성공한 것을 볼 수 있습니다.
테스트 코드가 무엇인지, JUnit과 AssertJ를 사용한 given-when-then 테스트 패턴에 대해 알아보았습니다. 테스트 코드를 작성하는 습관을 꼭 들이도록 합시다.