Springboot test file 작성하는 방법

nalaolla

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

프로젝트 개발정보

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

날아올라↗↗ 2017. 12. 4. 22:53

개요

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

라이브러리 설치

다음과 같이 스타터 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"))                                 

               .andDo(print())                                        

               .andExpect(status().isOk())                            

               .andExpect(content().contentType(HTML_CONTENT_TYPE));  

    }

    @Test

    public void test002BoardListRest() throws Exception {

        mockMvc.perform(get("/rest/list"))                                      

               .andExpect(content().contentType(JSON_CONTENT_TYPE))             

               .andExpect(jsonPath("$", iterableWithSize(10)))                  

               .andExpect(jsonPath("$[0]['num']", equalTo(190)))                

               .andExpect(jsonPath("$[0]['title']", containsString("테스트")))  

               ;

    }

    @Test

    public void test003BoardWriteSubmit() throws Exception {

        mockMvc.perform(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()))  

                       .param("title", "파일 업로드 테스트")

                       .param("contents", "내용은 여기에 들어갑니다..."))

               .andExpect(content().contentType(HTML_CONTENT_TYPE));

    }

  • 10~11행: andExpect 메서드에는 MockMvcResultMatchers 클래스의 메서드를 지정한다. (API 참고: //docs.spring.io/spring/docs/4.3.10.RELEASE/javadoc-api/org/springframework/test/web/servlet/result/MockMvcResultMatchers.html)
  • 18~20행: jsonPath 메서드는 JSON 요소를 따라가서 확인할 때 편리하게 쓰인다. (참고: //github.com/json-path/JsonPath)
  • 18~20행: iterableWithSize, equalTo, containsString 등의 Hamcrest 매처는 필수는 아니지만 테스트 코드를 보다 직관적으로 작성하게 도와준다. (참고: //code.google.com/archive/p/hamcrest/wikis/Tutorial.wiki)
  • 27~28행: POST 요청 시 기본 application/x-www-form-urlencoded로 전송할 파라미터를 지정한다. (<input type="hidden" name="title" value="테스트 게시글" />과 동일)
  • 35~36행: multipart/form-data로 POST 요청하여 파일을 업로드하는 URL은 MockMultipartFile 객체를 생성하여 멀티파트 업로드 파일을 모킹한다. (<input type="file" name="files" />와 동일)

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

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

세션이 필요한 URL 테스트

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

  1. (//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")                                    

@WithMockUser(username="admin123", roles={"USER","ADMIN"}))  

public void testXXX() throws Exception {

    ...

}

그 밖의 자세한 사용 방법은 //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 탭에 결과가 표시된다.

Toplist

최신 우편물

태그