Showing

[스프링부트] 단위 테스트 코드 작성, 롬복, Dto 본문

JAVA, SPRING/스프링 부트와 AWS로 혼자 구현하는 웹서비스

[스프링부트] 단위 테스트 코드 작성, 롬복, Dto

RabbitCode 2023. 6. 1. 05:53

*이동욱 저, 스프링부트와 aws로 혼자 구현하는 웹서비스를 학습하면서 작성한 포스팅입니다.

 

1. 테스트 코드

큰 규모의 서비스에서 테스트 코드의 작성은 매우 중요한 기술이자 습관이다.

(1) TDD vs 단위테스트

TDD는 테스트가 주도하는 개발이므로, 테스트 코드를 먼저 작성하는 것부터 시작한다.

레드그린사이클

red : 항상 실패하는 테스트를 먼저 작성하고

Green : 테스트가 통과하는 프로덕션 코드를 작성하고

Refactor : 테스트가 통과하면 프로덕션 코드를 리팩토링

 

 

반면, 단위테스트는 TDD의 첫번쨰 단계인 기능 단위의 테스트 코드를 작성하는 것을 이야기한다. TDD와 달리 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않는다. 순수하게 테스트 코드만 작성하는 것을 이야기 한다.

 

(2) 단위테스트를 작성하는 이유

- 단위 테스트는 개발단계 초기에 문제를 발견하게 도와준다

- 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있다

- 단위테스트는 기능에 대한 불확실성을 감소시킬 수 있다

- 단위 테스트는 시스템에 대한 실제 문서를 제공한다. 즉, 단위 테스트 자체가 문서로 사용할 수 있다.

 

즉, 단위테스트가 없을 때의 개발 방식은

 

_1 코드를 작성하고

_2 프로그램(톰캣)을 실행한뒤

_3 포스트맨과 같은 api 테스트 도구로 http 요청하고

_4 요청 결과를 system.out.println()으로 눈검증하고

_5 결과가 다르면 다시 프로그램(톰캣)을 중지하고 코드를 수정

 

여기서 _2~_5는 매번 코드를 수정할 때마다 반복해야한다. 톰캣 재시작 시간은 수십번씩 수정해야 하는 상황에서 아무런 코드 작업 없이 1시간 이상 소요되기도 한다. 결국 테스트 코드가 없으니 눈과 손으로 직접 수정된 기능을 확인할 수 밖에 없기 때문에 톰캣을 내렸다가 다시 실행하는 일을 반복하는 것이다.

테스트 코드를 작성하면 사람이 눈으로 검증하지 않게 자동검증이 가능하다. 단위 테스트를 실행만 하면 더는 수동 검증이 필요치 않게 된다.

개발자가 만든 기능을 안전하게 보호해주는데, 규모가 큰 서비스에서는 새로운 기능을 추가하면 기존에 잘 되던 다른 기능에 문제가 생길 수 있다. 그런데 기능들이 추가될 때마다 서비스의 모든 기능을 테스트할 수 없기 때문에 기존 기능이 잘 작동되는 것을 보장해주는 것이 테스트 코드이다. A라는 기존 기능에 기본 기능을 비롯해 여러 경우를 모두 테스트 코드로 구현해 놓았다면 테스트 코드를 수행만 하면 문제를 조기에 찾을 수 있기 떄문이다. 

 

 

2. 테스트 코드 작성하기

(1) @SpringBootApplication

테스트 코드 작성을 돕는 프레임 워크 중에 가장 대중적인 것은 xUnit이다. 개발환경에 따라 Unit 테스트를 도와주는 도구이고 자바용은 JUnit이다. 본 책(스프링부트와 aws로 혼자 구현하는 웹서비스)에서는 Junit4로 테스트 코드를 작성해보도록 한다.

 

일반적으로 패키지명은 웹사이트 주소의 역순으로 한다.

예를들어

admin.jojo.com이라는 사이트라면 패키지명은 com.jojo.admin으로 하면된다.

package com.jojo.book.springbootwebservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

위 캡처의 패키지를 만들고 Application 클래스를 생성한다. 코드는 위와 같이 작성해준다.

이 Application 클래스는 앞으로 만들 프로젝트의 메인 클래스가 된다.

@SpringBootApplication으로 인해 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성 모두 자동으로 설정된다.

특히나 @SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 이 클래스는 항상 프로젝트 최상단에 위치해야 한다.

main 메소드에서 실행하는 SpringApplication.run으로 인해 내장 WAS(Web Application Server, 웹 어플리케이션 서버)를 실행한다. 내장 was란 별도로 외부에 was를 두지 않고 애플리케이셔을 실행할 때 내부에서 WAS를 실행하는 것을 이야기한다. 이렇게 되면 항상 서버에 톰캣을 설치할 필요가 없게 되고, 스프링부트로 만들어진 Jar 파일(실행 가능한 Java 패키징 파일)로 실행하면 된다. 

* 책 후반부에 톰캣 없이 어떻게 배포하고 서비스할 수 있는지 설명할 것이다.

 

스프링부트에서는 내장 was를 사용하는 것을 권장한다. 이유는 '언제 어디서나 같은 환경에서 스프링 부트를 배포'할 수 있기 때문이다. 외장 WAS를 쓴다면 모든 서버는 WAS의 종류와 버전, 설정을 일치시켜야만 한다. 새로운 서버가 추가되면 모든 서버가 같은 WAS 환경을 구축해야만 한다. 

(2) 테스트를 위한 컨트롤러 생성

테스트를 위한 controller를 만들어보도록 한다.

 

캡처처럼 하위에 web이라는 패키지를 만든다. 앞으로 컨트롤러와 관련된 클래스들은 모두 이 web 패키지에 담는다.

이제 테스트해볼 컨트롤러를 만든다.

package com.jojo.book.springbootwebservice.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";
    }
}

코드 설명

package com.jojo.book.springbootwebservice.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어준다.

예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준다는 느낌으로 받아들이면 된다.
public class HelloController {
    @GetMapping("/hello") // HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어준다.

예전에는 @RequestMapping(method=RequestMethod.GET)으로 사용되었다. 이제 이 프로젝트는 /hello 요청이 오면 문자열 hello를 반환하는 기능을 가지게 되었다.
    public String hello(){
        return "hello";
    }
}

(3) 테스트 세팅 및 테스트

작성한 코드가 제대로 작동하는지 테스트하기 위해 WAS를 실행하지 않고, 테스트 코드로 검증해보도록 한다.

src/test/java 디렉토리에 src/main/java에서 생성했던 패키지를 그대로 다시 생성한다. 그리고 테스트를 작성할 클래스를 생성한다. 일반적으로 테스트 클래스는 대상 클래스 이름에 Test를 붙인다. 여기서는 HelloControllerTest로 생성한다. 그리고 다음과 같은 테스트 코드를 추가한다.

package com.jojo.book.springbootwebservice.web;
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.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;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;
    @Test
    public void hello가_리턴된다() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
}

코드 설명

package com.jojo.book.springbootwebservice;
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 org.springframework.test.web.servlet.ResultActions;
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;

@RunWith(SpringRunner.class) // 테스트 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행

여기서는 SpringRunner라는 스프링 실행자를 사용

즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할
@WebMvcTest(controllers = HelloController.class)

Web(Spring MVC)에 집중할 수 있는 어노테이션


public class HelloControllerTest {
    @Autowired // 스프링이 관리하는 빈(Bean)을 주입받는다
    private MockMvc mvc; // 웹 API를 테스트할 때 사용, 스프링 MVC 테스트의 시작점, 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.
    @Test
    public void hello가_리턴된다() throws Exception {
        String hello = "hello";
        mvc.perform(get("/hello")) MockMvc를 통해 /hello주소로 HTTP GET 요청을 한다.체이닝이 지원되어 여러 검증 기능을 이어서 선언가능
                .andExpect(status().isOk()) //mvc.perform 결과 검증, HTTP Header의 Status를 검증, 200, 404, 500등의 상태 검증

여기선 OK, 즉 200인지 아닌지 검증 
                .andExpect(content().string(hello)); //mvc.perform 결과 검증, 응답 본문의 내용 검증. Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증한다.


    }
}

Run Test
테스트가 성공했다.

테스트가 성공했다는 것은 .andExpect(status().isOk())와 .andExpect(content().string(hello)) 모두 통과했음을 의미한다.

 

(4) 수동 확인

수동으로도 실행해서 정상적으로 값이 출력되는지 확인해본다.

main 급인 Application을 실행한다.

 

스프링 부트 로그에서 톰캣 서버가 8080 포트로 실행되었다는 것이 출력된다.
잘 출력되고 있다..

 

테스트 코드 결과와 같은 것을 알 수 있다. 절대 수동으로 검증하고 테스트 코드를 작성하지는 않는다. 테스트 코드로 먼저 검증 후, 정말 믿을 수 없다는 생각이 들 때 프로젝트를 실행해 확인한다.

 

3. 롬복

롬복은 자바 개발자들의 필수 라이브러리이다. 롬복은 자바 개발할때 자주 사용하는 코드 Getter, Setter, 기본생성자, toString 등을 어노테이션으로 자동 생성해준다. 

특히 인텔리제이에선 플러그인 덕분에 쉽게 설정이 가능하다.

 

(1) 프로젝트에 롬복 추가

_1 build.gradle에 다음의 코드를 추가한다.

    implementation 'org.projectlombok:lombok'

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

_2 롬복 플러그인 설치

.ignore 설치가 마찬가지로 Action에서 플로그인 검색하여 플러그인 설치 팝업의 마켓 플레이스 탭에서 롬복을 설치한다.

_3 세팅 -> 빌드 -> 컴파일러 -> Enable annotation processing 체크

이제 이 프로젝트에서는 롬복을 사용할 수 있다. 롬복은 사실 프로젝트마다 설정해야 하므로 플러그인 설치는 한번만 하면 되지만, build.grable에 라이브러리를 추가하는 것과 Enable annotation processing을 체크하는 것은 프로젝트마다 진행해야 한다.

 

(2) Hello Controller 코드를 롬복으로 리팩토링

롬복으로 변경 후 문제가 생기는지는 테스트 코드만 돌려보면 알 수 있다.

먼저 web 패키지에 dto 패키지를 추가한다. 앞으로 모든 응답 Dto는 이 Dto 패키지에 추가한다. 이 패키지에 HelloResponseDto를 생성한다. 

package com.jojo.book.springbootwebservice.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

package com.jojo.book.springbootwebservice.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // 선언된 모든 필드의 get 메소드 생성
@RequiredArgsConstructor // 선언된 모든 final 필드가 포함된 생성자 생성, final이 없는 필드는 생성자에 포함되지 않음
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

 

<잠깐 Dto란,>

스프링에서 Dto는 Data Transfer Object의 약자로, 데이터 전송을 위한 객체. Dto는 주로 컨트롤러와 서비스 간의 데이터 전달을 담당하며, 데이터베이스에서 조회한 엔티티를 웹 API 응답으로 변환하거나, 클라이언트로부터 전달받은 데이터를 엔티티로 변환하는 등의 역할을 수행

Dto의 사용의 이점:

  1. 데이터 전달의 명확성: Dto는 원하는 데이터만을 선택하여 전달하여, 불필요한 데이터의 노출을 방지하고 응답의 크기를 최소화
  2. 레이어 간 의존성 분리: 엔티티와 Dto는 서로 다른 레이어에서 사용되기 때문에, 엔티티의 변경이 Dto에 영향을 주지 않는다. 이는 시스템의 유연성과 확장성을 높여준다.
  3. 클라이언트와의 호환성: Dto는 클라이언트와의 데이터 교환을 위한 계약(Contract) 역할을 수행. 클라이언트와의 API 버전 변경이나 데이터 구조 변경에 유연하게 대응할 수 있다.

Dto 클래스는 엔티티와 유사한 구조를 가지며, 필요한 데이터 필드와 접근자(getter) 및 설정자(setter) 메서드로 구성. 주로 Java Bean 규칙에 따라 작성되며, Lombok과 같은 라이브러리를 사용하여 코드를 간결하게 작성할 수도 있다.

예를 들어, 사용자 정보를 담는 User 엔티티가 있다고 가정한다면  경우 UserDto 다음과 같이 작성될 있다:

public class UserDto {
    private Long id;
    private String username;
    private String email;

    // 기본 생성자, 생성자, getter/setter 생략...

    // 추가적인 메서드나 유효성 검사 등을 포함할 수도 있다.
}

Dto 클래스를 사용하여 데이터 전송을 관리하면 엔티티와 Dto 간의 변환 작업이 필요할 있다. 이때 ModelMapper, MapStruct 등의 라이브러리를 사용하면 엔티티와 Dto 간의 변환을 자동화할 있다.

 

<여기서 엔티티란,>

엔티티(Entity)는 객체 지향 프로그래밍에서 도메인(Domain) 모델을 표현하는 클래스를 말한다. 엔티티는 데이터베이스의 테이블과 매핑되는 개념으로, 데이터베이스에서 저장 및 관리되는 데이터의 단위이다.

엔티티 클래스는 해당 도메인의 속성과 동작을 정의하며, 애플리케이션에서 업무 로직을 구현하는 데 사용된다.

예를 들어, 사용자(User) 정보를 다루는 애플리케이션을 만든다고 가정할 때 User 클래스는 엔티티로 정의될 있다. User 엔티티는 사용자의 아이디, 이름, 이메일 등의 속성을 가지며, 사용자 관련 동작(메서드) 정의할 있다.

public class User {
    private Long id;
    private String username;
    private String email;
    
    // 생성자, getter/setter, 동작 메서드 등...

    // 추가적인 동작 메서드나 비즈니스 규칙을 포함할 수 있다.
}

엔티티 클래스는 데이터베이스 테이블과 일치하는 필드를 포함하고 있다. 일반적으로 JPA(Java Persistence API)를 사용하는 경우, 엔티티 클래스에는 @Entity 어노테이션이 붙어야 하며, 필드들은 @Column, @Id, @GeneratedValue 등의 어노테이션을 사용하여 매핑 설정을 할 수 있다.

데이터베이스와의 상호작용을 위해 엔티티는 주로 Repository 패턴과 함께 사용된다. Repository는 엔티티의 영속성 관리와 데이터베이스 조작을 담당하며, 엔티티를 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있는 메서드를 제공한다.

엔티티는 도메인 모델의 핵심 요소이며, 애플리케이션의 비즈니스 로직을 담당하는 객체이다. 엔티티 클래스의 속성과 동작을 정의하여 애플리케이션을 구현하고, 데이터베이스와의 연동을 통해 데이터를 관리한다.

 

 

일반적으로 데이터베이스에서 사용자 정보를 저장하기 위한 테이블은 "User" 또는 "Users"와 같이 명명되고, 이러한 테이블은 애플리케이션에서 유저 엔티티를 매핑하기 위해 사용될 수 있다.

유저 엔티티는 앞서 설명한대로 사용자 정보를 포함하는 객체로 데이터베이스 테이블의 각 열은 엔티티 클래스의 속성에 해당하며, 각 행은 엔티티 객체의 인스턴스에 해당한다.

예를 들어, MySQL에서 "users"라는 테이블을 사용하여 사용자 정보를 저장한다고 가정., 해당 테이블은 사용자의 아이디(id), 이름(name), 이메일(email) 등과 같은 (column) 가지고 있다. 이러한 열과 매칭되는 유저 엔티티 클래스를 작성할 있다.

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    @Column(name = "email")
    private String email;
    
    // 생성자, getter/setter 등...
}

위의 예제에서는 JPA의 어노테이션을 사용하여 유저 엔티티를 매핑하였다. @Entity 어노테이션은 해당 클래스가 JPA 엔티티임을 표시하고, @Table 어노테이션은 데이터베이스 테이블과의 매핑 정보를 제공한다. @Column 어노테이션은 속성과 데이터베이스 열(column) 간의 매핑을 설정힌다. @Id 어노테이션은 엔티티의 주키(primary key)를 표시하고, @GeneratedValue 어노테이션은 주키의 값을 자동으로 생성하는 전략을 지정한다.

이렇게 유저 테이블과 매핑되는 유저 엔티티를 정의하고, JPA 사용하여 데이터베이스와의 상호작용을 구현할 있다.

 

(3) Dto 롬복 작동에 대한 테스트 코드 작성

package com.jojo.book.springbootwebservice.web.dto;
import com.jojo.book.springbootwebservice.web.HelloController;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {
    @Test
    public void 롬복_기능_테스트(){
        // 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);
    }
}

코드 설명

package com.jojo.book.springbootwebservice.web.dto;
import com.jojo.book.springbootwebservice.web.HelloController;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {
    @Test
    public void 롬복_기능_테스트(){
        // given
        String name = "test";
        int amount = 1000;

        // when
        HelloResponseDto dto = new HelloResponseDto(name, amount);

        // then
        assertThat(dto.getName()).isEqualTo(name); // assertj 라는 테스트 검증 라이브러리의 검증 메소드로, 검증하고 싶은 대상을 메소드 인자로 받는다. 메소드 체이닝이 지원되어 isEqualTo와 같이 메소드를 이어서 사용할 수 있다.
        assertThat(dto.getAmount()).isEqualTo(amount); // assertj 의 동등 비교 메소드, assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공
    }
}

 

Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용했다. assertj 역시 Junit에서 자동으로 라이브러리 등록을 해준다. assertJ 의 장점은,

- CoreMatchers와 달리 추가적으로 라이브러리가 필요치 않다.

  - Junit의 assertThat을 쓰게 되면 is()와 같이 CoreMatchers 라이브러리가 필요하다.

 

- 자동완성이 좀 더 확실하게 지원된다.

 - IDE 에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약하다. 

https://youtu.be/zLx_fI24UXM

추가로, build.gradle을 아래와 같이 바꿔줘야 한다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

높은 버전의 그래이들에서 책을 따라하는 경우, anootationProcessor에 대한 종속성을 따로 선언해주어야 하기 때문이다.

https://github.com/binchoo/spring-boot-210523/issues/4

 

[컴파일 오류] Lombok 생성자 작성 오류 · Issue #4 · binchoo/spring-boot-210523

동일이슈: jojoldu/freelec-springboot2-webservice#78 (comment) 챕터: #2 [이슈 내용] 롬복 기능 테스트 실패 [이슈 원인] Gradle 버전이 높아서 생기는 호환 문제

github.com

 

그러면 아래 캡처와 같이 테스트를 통과하는 것을 확인할 수 있다.

롬복 테스트가 정상적으로 돌아간다는 것은 롬복의 @Getter로 get 메소드가, @RequiredArgsConstructor로 생성자가 자동으로 생성된다는 뜻이다. 

 

(4) HelloController에 새로만든 ResponseDto를 사용하도록 코드 추가

package com.jojo.book.springbootwebservice.web;
import com.jojo.book.springbootwebservice.web.dto.HelloResponseDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(@RequestParam("name") String name,
                                     @RequestParam("amount") int amount){
        return new HelloResponseDto(name, amount);
    }
}

코드 설명

    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(@RequestParam("name") String name,
                                     @RequestParam("amount") int amount){
        return new HelloResponseDto(name, amount);
    } 

RequestParam : 외부에서 api로 넘긴 파라미터를 가져오는 어노테이션이다. 여기서는 외부에 name (@RequestParam("name") 이란 이름으로 넘긴 파라미터를 메소드 파라미터 name(String name)에 저장하게 된다.

 

(4) HelloControllerTest에 테스트 코드 추가

name 과 amount는 API를 호출하는 곳에서 넘겨준 값들이다. 추가된 API를 테스트하는 코드를 HelloControllerTest에 추가한다. 

package com.jojo.book.springbootwebservice.web;
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.Matchers.is; // 추가
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;
    @Test
    public void hello가_리턴된다() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
    @Test
    public void helloDto가_리턴된다() 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)));
    }
}

코드분석


    @Test
    public void helloDto가_리턴된다() throws Exception {
        String name = "hello";
        int amount = 1000;

        mvc.perform(
                        get("/hello/dto")
                                .param("name", name) //param API 테스트할 때 사용된 요청 파라미터를 설정한다. 단 값은 String만 허용된다. 그래서 날짜/숫자/금액 등의 데이터도 등록할 때는 문자열로 변경해야만 가능하다.
                                .param("amount", String.valueOf(amount))) //param
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name))) // jsonPath : JSON 응답값을 필드별로 검증할 수 있는 메소드이다. $를 기준으로 필드명을 명시한다. 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증한다.
                .andExpect(jsonPath("$.amount", is(amount)));
    }

테스트 결과

JSON이 리턴되는 API 역시 정상적으로 테스트가 통과하는 것을 확인할 수 있다.