스프링 부트에서 테스트 코드 작성
1. 테스트 코드가 필요한 이유
- 빠른 피드백
기존의 개발 방식 다음과 같다.- 코드 작성
- 프로그램 실행
- API 테스트 도구(Postman 등)로 HTTP 요청(POST, GET, PUT 등등)
- 요청 결과를 눈으로 검증
- 결과가 의도대로 나오지 않으면, 다시 프로그램을 중지하고 코드 수정
이 과정에서 톰캣(서버)를 재시작 해야하는데, 규모가 큰 서버의 경우 서버를 껐다가 키는 것은 많은 시간을 소요하기 때문에 비효율적이다.
수동 검증 -> 자동 검증
사람의 눈으로 검증(수동 검증)하지 않아도 되기 때문에 더 확실한 검증이 가능하다.- 개발자가 만든 기능을 안전하게 보호
하나의 기능을 수정했을 때, 나머지의 기능도 잘 동작되는지 확인하고, 서비스의 모든 기능을 보장해준다.
2. HelloContorller 테스트 코드 작성하기
책에서는 프로젝트를 생성한 뒤, Application 클래스를 만들어서 직접 코드를 작성하고 있지만,
지금은 인텔리제이에서 프로젝트를 생성하면, Application 파일을 자동으로 만들어줍니다.
- Application
1
2
3
4
5
6
7
8
9
10
11
package com.example.spring_study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // 스프링 부트 자동 설정과 Bean 관리
public class SpringStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringStudyApplication.class, args);
}
}
- HelloController
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.spring_study.web;
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";
}
}
서버를 실행한 뒤, https://localhost:8080/hello를 접속하면 hello라는 글자가 정상적으로 출력됨을 확인할 수 있다.
그렇다면 이제 이렇게 눈으로만 검증했던 이 HelloController를 테스트 코드를 통해 검증해보자.
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
package com.example.spring_study.web;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
//@ExtendWith(SpringExtension.class) // JUnit5
//@WebMvcTest(HelloController.class)
@AutoConfigureMockMvc // web 계층 테스트를 할 수 있도록 자동 설정
@SpringBootTest // 애플리케이션 전체 컨텍스트를 로드
class HelloControllerTest {
@Autowired
private MockMvc mvc;
@DisplayName("hello가 리턴된다.")
@Test
public void helloTest() throws Exception {
String hello = "hello";
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
}
책에서는
1
2
@ExtendWith(SpringExtension.class) // JUnit5
@WebMvcTest(HelloController.class)
이렇게 사용하고 있다. 하지만, @WebMvcTest(HelloController.class)는 내부적으로 @ExtendWith(SpringExtension.class)을 이미 포함하고 있기 때문에 @WebMvcTest(HelloController.class)만 사용해도 된다.
평소에 @SpringBootTest라는 어노테이션을 많이 썼었는데, 이 둘의 차이는 다음과 같다.
@WebMvcTest(HelloController.class)
: 컨트롤러 레이어만 신속하게 테스트한다. (단위 테스트)@SpringBootTest
: Spring Boot 애플리케이션의 전체 컨텍스트를 로드합니다. 즉, 애플리케이션에서 사용하는 모든 빈을 로드하여 통합 테스트를 실행할 수 있습니다.
즉, 해당 컨트롤러만 독립적으로 테스트를 진행하고 싶다면, @WebMvcTest(HelloController.class)을 이용하면 되고, 통합적으로 테스트를 해보고싶다면, @SpringBootTest를 붙여주면 된다.
위의 코드에 대해 자세히 설명해보자면, 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 서버를 실제로 배포하지 않고도, HTTP 요청을 만들어서 컨트롤러를 테스트할 수 있게 해주는 MockMvc 객체를 주입
@Autowired
private MockMvc mvc;
// 요즘 Junit에는 @DisplayName이 추가되어서, 테스트의 이름을 따로 명시할 수 있게 되어있음.
@DisplayName("hello가 리턴된다.")
@Test
public void helloTest() throws Exception {
String hello = "hello";
// mvc.perform(get("/hello")): 아까 주입해줬던 MockMvc를 통해 /hello로 get 요청을 보낸다.
// status().isOk() : 그리고 그 요청의 결과가 성공적인지 검증하고,
// content().string(hello) : 응답 본문이 hello인지 검증한다.
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
테스트 코드를 실행해보면, 아래와 같이 성공했음을 확인할 수 있다.
3. 롬복 소개 및 설치하기
롬복(Lombok)은 코드를 깔끔하게 보이기 위해서 사용되는 라이브러리이다.
롬복을 이용하면, 클래스를 생성할 때 자주 사용하는 getter, setter, toString 등의 메서드를 자동으로 생성해준다.
설정 방법
아래와 같이 build.gradle 파일의 dependencies에 compileOnly 'org.projectlombok:lombok'
를 추가해주면 된다.
1
2
3
4
5
6
7
8
9
10
11
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Lombok
compileOnly 'org.projectlombok:lombok'
}
그리고 인텔리제이에서 Settings > Plugins로 이동해서 Lombok을 검색하고 설치한 뒤, 인텔리제이를 재시작한다.
그리고 다시 Settings > Build, Execution, Deployment > Compiler > Annotation Processors로 이동해서 Enable annotation processing를 체크해야 Lombok이 제대로 동작한다.
4. HelloController 코드를 롬복으로 전환하기
- Lombok 미사용 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.spring_study.web.dto;
public class HelloControllerDto {
private final String name;
private final int amount;
public HelloControllerDto(String name, int amount) {
this.name = name;
this.amount = amount;
}
public String getName() {
return name;
}
public int getAmount() {
return amount;
}
}
- Lombok 사용 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.spring_study.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // getter 자동 생성
@RequiredArgsConstructor // final이 붙은 필드가 포함된 생성자를 자동 생성
public class HelloControllerDto {
private final String name;
private final int amount;
}
비교해보면 알 수 있듯이, Lombok을 사용하면 코드가 간결해지고 가독성이 높아진다.
그렇다면 이 간결해진 코드가 잘 동작하는지 테스트 해보자.
- HelloResponseDtoTest
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
package com.example.spring_study.web.dto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class HelloResponseDtoTest {
@DisplayName("롬복 기능 테스트")
@Test
public void LombokTest() {
// given
String name = "test";
int amount = 1000;
// when
HelloResponseDto dto = new HelloResponseDto(name, amount);
// then
assertThat(dto.getName()).isEqualTo(name);
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
실행시켜보면, 다음과 같이 기능이 정상적으로 동작함을 알 수 있다.
- HelloController에 ResponeDto를 사용하는 코드를 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") Integer amount) {
return new HelloResponseDto(name, amount);
}
}
@RequestParam
: url로 파라미터를 보내주면 이 어노테이션이 그 값을 가져온다.
이를 다시 한 번 테스트 코드로 작성해보자.
- HelloControllerTest 최종 코드
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.spring_study.web;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
//@ExtendWith(SpringExtension.class) // JUnit5
//@WebMvcTest(HelloController.class)
//@AutoConfigureMockMvc // web 계층 테스트를 할 수 있도록 자동 설정
//@SpringBootTest // 애플리케이션 전체 컨텍스트를 로드
@WebMvcTest(HelloController.class)
class HelloControllerTest {
@Autowired
private MockMvc mvc;
@DisplayName("hello가 리턴된다.")
@Test
public void helloTest() throws Exception {
String hello = "hello";
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
@DisplayName("helloDto가 리턴된다.")
@Test
public void helloDtoTest() throws Exception {
String name = "hello";
int amount = 1000;
mvc.perform(
get("/hello/dto")
.param("name", name)
.param("amount", String.valueOf(amount))
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.name",is(name)))
.andExpect(jsonPath("$.amount",is(amount)));
}
}
param
: get()을 이용하여 api를 테스트할 때, 요청 파라미터를 설정할 수 있게 해준다. 값은 String만 허용하기 때문에, int형의 경우 String.valueOf()으로 타입을 변환해줘야 한다.
jsonPath
: json 응답을 필드 별로 검증할 수 있다.
[참고] Postman으로 눈으로 확인해보기
- helloTest()
- helloDtoTest()