Spring Boot web layer 테스트

평소 테스트에 관심은 있지만 하는 법을 잘 몰라 몇 번 시도 후 포기하기를 수차례였는데
Cloud Native Java를 읽던 중 Spring Guide를 알게되서 web layer test 가이드가 있어 이 내용을 정리한다.
스프링 부트 web layer를 테스트하기 위해 아래와 같이 web과 test dependency를 추가한다.

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

먼저 Controller를 생성한다.
아래와 같이 service단은 없는 단순한 구조의 controller 이다.

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
}

먼저 HelloController가 bean으로 등록되있는지를 테스트하는 간단한 스모크 테스트이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.example.demo.HelloController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNull.notNullValue;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SmokeTest {
@Autowired
private HelloController helloController;

@Test
public void controller_autowired_테스트() {
assertThat(helloController, notNullValue());
}

}

@SpringBootTest를 통해 테스트 시 application이 실행되어 IOC container에 bean으로 등록돼있는 HelloController를 주입받을 수 있다.


다음으로 HelloContoller의 /hello로 요청을 할 경우 Hello World를 응답하는지를 테스트한다.

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
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;

@Test
public void helloWorld_테스트() {
String response = restTemplate.getForObject("http://localhost:" + port + "/hello", String.class);

assertThat(response, is("Hello World"));
}
}

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)를 통해 서버를 무작위 포트로 실행시킨다.
또한 테스트 시 Spring Boot가 TestRestTemplate을 제공하기 때문에 @Autowired로 주입받아 사용가능하다.


위 테스트의 경우 테스트는 잘 실행되지만 서버 전체를 실행하기 때문에 단위 테스트로는 과하다.
아래 방법을 통해서 서버를 실행시키지 않고 테스트하려는 레이어만 테스트할 수 있다.
(Spring application context는 실행되지만 server는 실행되지 않는다.)

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
import org.junit.Test;
import org.junit.runner.RunWith;
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.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.core.Is.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ApplicationTest {
@Autowired
private MockMvc mockMvc; // @AutoConfigureMockMvc를 통해 주입받을 수 있다.

@Test
public void helloWorld_테스트() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(is("Hello World")));
}
}

하지만 위 테스트는 서버는 실행되지 않지만 전체 Spring application context가 실행된다.
@WebMvcTest를 사용하면 더 좁은 단위로 테스트가 가능하다.

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
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.core.Is.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest
public class WebLayerTest {
@Autowired
private MockMvc mockMvc;

@Test
public void helloWorld_테스트() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(is("Hello World")));
}
}

위 테스트들은 의존성이 없는 controller 뿐인 단순한 구조에서 테스트를 진행한 것이다.
보다 현실적인 application을 테스트하기 위해 아래와 같이 HelloService를 추가한다.

1
2
3
4
5
6
7
8
import org.springframework.stereotype.Service;

@Service
public class HelloService {
public String hello() {
return "Hello World";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class
HelloController {
private HelloService helloService;

public HelloController(HelloService helloService) {
this.helloService = helloService;
}

@GetMapping("/hello")
public String hello() {
return helloService.hello();
}
}

앞서 진행한 WebLayerTest의 경우 web layer를 인스턴스화 한 것이기 때문에 하나의 controller를 테스트하기에는 과하다고 볼 수 있다.
아래와 같이 @WebMvcTest(HelloController.class)를 통해 하나의 controller만을 인스턴스화하여 테스트할 수 있다.

@MockBean을 통해 HelloServiced의 mock 객체를 주입받을 수 있다.
(이 작업을 하지 않으면 application context를 실행시킬 수 없다.)

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
import com.example.demo.HelloController;
import com.example.demo.HelloService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.core.Is.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class WebMockTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private HelloService helloService;

@Test
public void name() throws Exception {
when(helloService.hello()).thenReturn("Hello Mock");

mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(is("Hello Mock")));
}
}
Share