관리 메뉴

nalaolla

스프링 부트 테스트 작성 가이드 본문

프로젝트 개발정보

스프링 부트 테스트 작성 가이드

날아올라↗↗ 2017. 12. 4. 22:53
728x90
반응형

개요

스프링 부트로 생성한 프로젝트에서 테스트 코드를 작성하는 예제 코드를 제시한다.


라이브러리 설치

다음과 같이 스타터 spring-boot-starter-test를 pom.xml에 test 스코프로 명시하면 테스트 관련 의존성 라이브러리가 자동으로 삽입된다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>


  • JUnit — 자바 표준 단위 테스트 프레임워크
  • Spring Test — 스프링 부트 애플리케이션 테스트 지원을 위한 유틸리티
  • AssertJ — 어셜선 라이브러리
  • Hamcrest — 유용한 매처를 지원하는 객체 라이브러리
  • Mockito — 자바 모킹 프레임워크
  • JSONassert — JSON 어셜션 라이브러리
  • JsonPath — JSON 구조를 탐색할 때 유용한 라이브러리


테스트 작성 원칙

1. 스프링 부트 폴더 구조에 따라 /src/test/java 이하에 패키지 구조에 맞게 작성한다.

2. 컨트롤러(호출 URL) 단위로 작성하되, 테스트 커버리지를 높이기 위해 컨트롤러에서 호출되지 않는 여타 소스 코드(예: 유틸리티)에 대한 테스트 코드도 별도로 작성한다.


JUnit 테스트의 전체 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
...
 
 
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class XXXControllerTest {
 
    private MockMvc mockMvc;
 
    @Autowired
    private WebApplicationContext webApplicationContext;
 
    @Before
    public void setup() throws Exception {
        mockMvc = webAppContextSetup(webApplicationContext).build();
    }
 
    @Test
    public void test001XXXList() throws Exception {
        mockMvc.perform(get("/list"))
               .andDo(print())
               .andExpect(status().isOk())
               .andExpect(content().contentType(HTML_CONTENT_TYPE));
    }
    @Test
    public void test002XXXList() throws Exception {
        mockMvc.perform(get("/otherList"))
               ...
    }
 
    ......
 
}
  • 10~11행: @RunWith(SpringRunner.class), @SpringBootTest는 필수 어노테이션이다.
  • 12행: 테스트가 다수일 경우, 메서드 명의 알파벳 순서로 실행하기 위한 설정이며, 테스트 메서드는 test001XXX, test002XXX, ... 식으로 명명하면 간편하다.
  • 25행: 각 테스트(@Test) 실행 전후로 실행해야 할 구성(config)/정리(teardown) 코드는 각각 @Before, @After 어노테이션을 붙인 메서드에 구현한다.
  • 27행: 모든 테스트는 실제 웹 애플리케이션 컨텍스트를 모킹한 MockMvc 객체를 이용하여 수행한다.


기본적인 MVC 단위 테스트

단위 테스트는 대부분 컨트롤러에 구현한 HTTP 요청 단위의 메서드가 정상적인 응답을 주는지 확인하는 용도일 것이다. 다음 예제 코드를 참고하여 적절한 테스트 코드를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...
    private static MediaType HTML_CONTENT_TYPE = new MediaType(MediaType.TEXT_HTML.getType(), MediaType.TEXT_HTML.getSubtype(), Charset.forName("utf8"));
    private static MediaType JSON_CONTENT_TYPE = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
 
 
    @Test
    public void test001XXXList() throws Exception {
        mockMvc.perform(get("/list"))                                 // GET /list를 요청
               .andDo(print())                                        // 응답 내용을 출력
               .andExpect(status().isOk())                            // 응답 코드가 200(OK)인지 확인
               .andExpect(content().contentType(HTML_CONTENT_TYPE));  // 컨텐트 타입이 text/html인지 확인
    }
 
    @Test
    public void test002BoardListRest() throws Exception {
        mockMvc.perform(get("/rest/list"))                                      // GET /rest/list로 REST API를 호출
               .andExpect(content().contentType(JSON_CONTENT_TYPE))             // 컨텐트 타입이 application/json인지 확인
               .andExpect(jsonPath("$", iterableWithSize(10)))                  // JSON 배열의 크기가 10인가?
               .andExpect(jsonPath("$[0]['num']", equalTo(190)))                // JSON 배열의 첫 번째 원소의 num 프로퍼티 값이 190인가?
               .andExpect(jsonPath("$[0]['title']", containsString("테스트")))  // JSON 배열의 첫 번째 원소의 title 프로퍼티 값이 "테스트"인가?
               ;
    }
 
    @Test
    public void test003BoardWriteSubmit() throws Exception {
        mockMvc.perform(post("/write/submit")                            // POST /write/submit를 요청
                       .param("title""테스트 게시글")                   // 폼 파라미터 값 설정
                       .param("contents""내용이 여기에 들어갑니다..."))
               .andExpect(content().contentType(HTML_CONTENT_TYPE));
    }
 
    @Test
    public void test004BoardWriteSubmitWithFileUpload() throws Exception {
        mockMvc.perform(fileUpload("/write/submit")
                       .file(new MockMultipartFile("files""dummyFile01""text/plain""dummyFile01_Contents".getBytes()))  // 목 멀티파트 파일 객체를 지정한다.
                       .file(new MockMultipartFile("files""dummyFile02""text/plain""dummyFile02_Contents".getBytes()))  // MockMultipartFile(원소명, 파일명, 컨텐트 타입, 컨텐트 스트림)
                       .param("title""파일 업로드 테스트")
                       .param("contents""내용은 여기에 들어갑니다..."))
               .andExpect(content().contentType(HTML_CONTENT_TYPE));
    }


@RequestBody로 JSON 형식으로 입력을 받는 API 끝점(end-point)은 다음과 같이 Jackson 라이브러리의 ObjectMapper 객체를 이용하여 테스트한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
    private ObjectMapper objectMapper;
 
 
    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
        objectMapper = new ObjectMapper();
    }
...
    @Test
    public void testCreateItem() throws JsonProcessingException, Exception {
        Item item = new Item();
        item.setId("Id");
        item.setName("Name");
        ......
 
        mockMvc.perform(post("/item")
                .contentType(JSON_CONTENT_TYPE)
                .content(objectMapper.writeValueAsString(item)))
        .andDo(print())
        .andExpect(status().isOk());
    }
  • 8행: ObjectMapper 객체를 생성한다.
  • 13~15행: POST 요청으로 보낼 자바 객체를 세팅한다.
  • 20행: 자바 객체를 문자열로 변환하여 자동으로 JSON 형식으로 만들어 준다.


공통적인 내용 설정

클래스의 각 테스트마다 반복되는 공통적인 내용은 @Before를 붙인 구성 메서드에 다음과 같이 선언하면 코드를 줄일 수 있다.

1
2
3
4
5
6
7
8
@Before
public void setup() throws Exception {
    mockMvc = webAppContextSetup(webApplicationContext)
                  .alwaysDo(print())
                  .alwaysExpect(status().isOk())
                  ...
                  .build();
}
  • 4행: 각 테스트마다 수신한 응답 결과를 로깅한다.
  • 5행: 각 테스트마다 HTTP 응답 코드가 200인지 확인한다.


서비스 목업 테스트

서비스 구현이 아직 덜 된 상태에서 컨트롤러 간 흐름을 테스트해야 할 경우, 다음과 같이 서비스단을 목업(mock-up)할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
    @MockBean
    private BoardService boardService;
...
    @Test
    public void testBoardTotalCount() throws Exception {
 
        given(boardService.selectBoardTotalCount()).willReturn(190);
 
        mockMvc.perform(get("/rest/list"))
               .andExpect(content().contentType(JSON_CONTENT_TYPE))
               ...
               .andExpect(jsonPath("$[0]['num']", equalTo(190)));
    }
  • 2~3행: /rest/list URL에 매핑된 컨트롤러는 내부적으로 BoardService 인터페이스에 구현된 메서드를 호출한다. 하지만, 아직 메서드 구현 전이라고 가정하고 이 서비스에 @MockBean 어노테이션을 붙여 목업한다.
  • 8행: 만약 boardService.selectBoardTotalCount() 메서드가 호출되면(given), 190이라는 값을 반환한다는(willReturn) 식으로 반환 데이터를 하드 코딩한다.
  • 13행: 실제로 /rest/list를 호출하여 컨트롤러를 테스트하면 8행에서 설정한 내용 덕분에 구현된 로직은 없지만 190이라는 값이 반환됨을 알 수 있다.


다른 마이크로서비스의 API를 호출하는 테스트의 경우, 해당 서비스를 목업하는 것은 불가능하므로 테스트를 진행하려면 해당 개발팀에 의뢰하여 개발이 완료되기 전까지 목 데이터를 반환하는 컨트롤러를 작성해달라고 요청해야 한다.


참고: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-mocking-beans


세션이 필요한 URL 테스트

사용자 세션이 전제된 URL을 테스트할 경우,

  1. (http://52.78.133.70/wiki/x/wIyl의 'Local Session 개발 가이드'를 참고하여) 로컬 세션을 생성하여 테스트하거나,
  2. spring-security-test 라이브러리가 제공하는 @WithMockUser 등의 어노테이션을 이용하는,

두 가지 방법이 있다. 1번 방법을 따르면 WebApplicationContext 생성 시 자동으로 세션이 생성되므로 별도의 작업이 필요 없으므로 2번 방법만 설명한다.


spring-security-test는 스프링 부트 스타터(spring-boot-starter-security, spring-boot-starter-test)에 포함되어 있지 않으므로 별도로 pom.xml에 명시해야 한다.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>


@Test 다음에 @WithMockUser을 붙이면 기본적으로 username이 "user", password가 "password", roles는 {"ROLE"}인 목 사용자 세션을 생성한다. @PreAuthorize("isAuthenticated()") 등으로 세션을 전제한 컨트롤러 메서드를 테스트할 때 간편하게 사용할 수 있는 방법이다.

1
2
3
4
5
6
7
8
9
...
import org.springframework.security.test.context.support.WithMockUser;
...
 
    @Test
    @WithMockUser
    public void testXXX() throws Exception {
        ...
    }


목 사용자 세션을 좀 더 상세하게 설정하려면 다음과 같이 어노테이션 속성에 값을 넣는다.

1
2
3
4
5
6
@Test
@WithMockUser("admin123")                                    // username이 "admin123"인 목 사용자 세션 생성
@WithMockUser(username="admin123", roles={"USER","ADMIN"}))  // username이 "admin123"이고 "USER", "ADMIN" 두 권한을 부여받은 목 사용자 세션 생성
public void testXXX() throws Exception {
    ...
}


그 밖의 자세한 사용 방법은 https://docs.spring.io/spring-security/site/docs/current/reference/html/test-method.html를 참고하여 작성한다.


트랜잭션 자동 롤백

트랜잭션이 수반되는 서비스 테스트 시 실행 후 테스트 결과 데이터를 남기고 싶지 않을 경우, 다음과 같이 @Test 다음에 @Transactional 어노테이션을 붙인다. SpringJUnit4ClassRunner는 해당 테스트의 실행 전 새로운 트랜잭션을 생성하고 실행 후 자동으로 롤백시킨다. (물론, 타 API 모듈의 REST API를 호출할 때에는 이와 같은 내용이 적용되지 않는다)

1
2
3
4
5
@Test
@Transactional
public void testXXX() throws Exception {
    ...
}


테스트 클래스에 모두 적용하려면 다음과 같이 클래스 앞에 @Transactional을 붙인다.

1
2
3
4
5
6
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class XXXControllerTest {
    ...
}


이클립스에서 단위 테스트 실행

테스트 클래스 작성 후 단축키 Alt X, T (Run Unit Test)를 누르면 단위 테스트가 자동으로 실행되고 잠시 후 JUnit 탭에 결과가 표시된다.

728x90
반응형