Post

스프링 부트에서 테스트 코드 작성

1. 테스트 코드가 필요한 이유

  • 빠른 피드백
    기존의 개발 방식 다음과 같다.
    1. 코드 작성
    2. 프로그램 실행
    3. API 테스트 도구(Postman 등)로 HTTP 요청(POST, GET, PUT 등등)
    4. 요청 결과를 눈으로 검증
    5. 결과가 의도대로 나오지 않으면, 다시 프로그램을 중지하고 코드 수정

    이 과정에서 톰캣(서버)를 재시작 해야하는데, 규모가 큰 서버의 경우 서버를 껐다가 키는 것은 많은 시간을 소요하기 때문에 비효율적이다.

  • 수동 검증 -> 자동 검증
    사람의 눈으로 검증(수동 검증)하지 않아도 되기 때문에 더 확실한 검증이 가능하다.

  • 개발자가 만든 기능을 안전하게 보호
    하나의 기능을 수정했을 때, 나머지의 기능도 잘 동작되는지 확인하고, 서비스의 모든 기능을 보장해준다.

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";
    }
}

1

서버를 실행한 뒤, 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));
}


테스트 코드를 실행해보면, 아래와 같이 성공했음을 확인할 수 있다.

1

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이 제대로 동작한다.

1

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);
    }

}


실행시켜보면, 다음과 같이 기능이 정상적으로 동작함을 알 수 있다.

1


  • 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 응답을 필드 별로 검증할 수 있다.

1
DtoTest 또한 성공정으로 테스트 할 수 있었다.


[참고] Postman으로 눈으로 확인해보기


  • helloTest()

1


  • helloDtoTest()

1

This post is licensed under CC BY 4.0 by the author.