본문 바로가기
Web/SpringBoot

2. SpringBoot에서 테스트 코드 작성

by SeleniumBindingProtein 2022. 2. 26.
728x90
반응형

1. 테스트 코드 

  - TDD : 테스트가 주도하는 개발로, 테스트 코드를 먼저 작성하는 것

  - 단위 테스트 : TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것

                          TDD와 달리 테스트 코드를 꼭 먼저 작성하지 않아도 되며, 리팩토링도 포함되지 않음

 

2. 단위 테스트 

    - 개발단계 초기에 문제 발견을 도와줌

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

    - 기능에 대한 불확실성을 감소시킬 수 있음

    - 시스템에 대한 실제 문서를 제공하며, 단위 테스트 자체가 문서로 사용할 수 있음

 

3. 테스트 코드 작성을 도와주는 프레임워크(xUnit)

    - 개발환경(x)에 따라 Unit 테스트를 도와주는 도구

    - JUnit - Java

    - DBUnit - DB

    - CppUnit - C++

    - NUnit - .net

 

4. Hello Controller 테스트 코드 작성

    - 프로젝트 패키지 및 메인 클래스 생성

    - @SpringBootApplication : 스프링 부트의 자동 설정, 스프링 Bean 읽기/생성을 모두 자동으로 설정됨

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

    - main 메소드에서 실행하는 SpringApplication.run으로 인해 내장 WAS(Web Application Server)를 실행

    - 내장 WAS : 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하며, 

                          항상 서버에 톰캣을 설치할 필요가 없게 되고, 스프링 부트로 만들어진 Jar 파일로 실행하면 됨

    - 내장 WAS의 장점은 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있음 

//Application.java

package com.jojoldu.book.springboot;

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

    - 테스트를 위한 Controller 생성

    - Controller와 관련된 클래스들은 web 패키지에 담음

    - @RestController : 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어줌

                                       이전 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 만들었음

    - @GetMapping : HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어줌 

                                   이전 @RequestMapping(method = RequestMethod.GET)으로 사용되었지만,

                                   현재 프로젝트는 /hello로 요청이 오면 문자열 hello를 반환하는 기능을 가지게 되었음

//HelloController.java

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.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 // 1
public class HelloController {

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

    - 테스트 코드를 작성할 클래스 생성

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

                                                                SpringRunner라는 스프링 실행자를 사용하며,

                                                                스프링 부트 테스트와 JUnit 사이에 연결자 역할을 함

    - @WebMvcTest : 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션으로, 

                                   선언할 경우, @Controller, @ControllerAdvice 등을 사용할 수 있지만,

                                   @Service, @Component, @Repository 등은 사용할 수 없음

    - @Autowired : 스프링이 관리하는 Bean을 주입 받음

    - private MockMvc mvc : 웹 API를 테스트할 때 사용하며, 스프링 MVC 테스트의 시작점이고,

                                                이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있음 

    - mvc.perform(get("/hello")) : MockMvc를 통해 /hello 주소로 HTTP GET 요청을 하며,

                                                        체이닝이 지원되어 여러 검증 기능을 이어서 선언할 수 있음

                                                        메소드 체이닝이란 여러 메소드를 이어서 호출하는 문법이며,

                                                        메소드가 객체(this)를 반환하여 여러 메소드를 순차적으로 선언할 수 있도록 함

    - .andExpect(status().isOk()) : mvc.perform의 결과를 검증하고, HTTP Header의 Status를 검증함 

    - .andExpect(content().string(hello)) : mvc.perform의 결과를 검증하고, 응답 본문의 내용을 검증함 

                                                                       Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증함

//HelloControllerTest.java

package com.jojoldu.book.springboot.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.*;
import static org.hamcrest.Matchers.is;
@RunWith(SpringRunner.class) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {

    @Autowired // 3
    private MockMvc mvc; // 4

    @Test 
    public void hello가_리턴된다() throws Exception{
        String hello = "hello";
            // 5                                // 6                       // 7
        mvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string(hello));
    }
}

[테스트 메소드 실행]

[main 메소드 실행]

==> 테스트 메소드 실행 때와 마찬가지로 스프링 부트 로그가 보이며, 톰캣 서버 8080 포트로 실행되었다는 것도 로그에 출력됨


    - 자바 개발자들의 필수 라이브러리 롬복

    - 롬복 설정을 위해 'Enable annotation processing' 체크

​  - build.gradle​​

//build.gradle

buildscript {
    ext{   //build.gradle에서 사용하는 전역변수를 설정하겠다는 의미
        springBootVersion = '2.1.7.RELEASE' //spring-boot-gradle-plugin라는
        // 스프링부트그레이들 플러그인의 2.1.7RELEASE를 의존성으로 받겠다.
    }

    repositories {  //각종 의존성(라이브러리)들을 어떤 원격 저장소에서 받을지 정한다.
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin : 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'     //스프링부트의 의존성을 관리해주는 플러그인!!!중요!!

group 'com.book'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-web')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    // 롬복 버전 5.x 이상임으로 아래 롬복 의존성 라이브러리를 추가함
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    // 롬복 버전 5.x 미만은 아래 롬복 의존성 라이브러리를 사용함
    // implementation 'org.projectlombok:lombok'
	// Gradle을 통해서 롬복 의존성 라이브러리를 추가해주었지만 동작하지 않는다면 
    // 자신의 Gradle 버전이 5.x 이상인지 확인해주셔야합니다.
	// 그 이유는 Gradle 버전이 올라가면서 Lombok 의존성을 추가하는 방법이 바뀌었기 때문입니다.
}

test {
    useJUnitPlatform()
}

​  - HelloController 코드를 롬복으로 전환

  - 모든 응답 Dto는 이 Dto 패키지에 추가

​  - @RequiredArgsConstructor : 선언된 모든 final 필드가 포함된 생성자를 생성해주며, final이 없는 필드는 생성자에 포함되지 않음​​

//HelloResponseDto.java

package com.jojoldu.book.springboot.web.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter // 1
@RequiredArgsConstructor // 2
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

 

​  - Dto에 적용된 롬복이 작동하는지 테스트 코드를 통해 확인

​  - assertThat : assertj라는 테스트 검증 라이브러리의 검증 메소드이며, 검증하고 싶은 대상을 메소드 인자로 받음

                          메소드 체이닝이 지원되어 isEqualTo와 같이 메소드를 이어서 사용할 수 있음

​  - isEqualTo : assertj의 동등 비교 메소드로, assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공

​  - assertj의 장점 : CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않으며, 자동완성이 좀 더 확실하게 지원됨

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

             IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약함​​​​​

//HelloResponseDtoTest.java

package com.jojoldu.book.springboot.web.dto;

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

[dto 테스트 메소드 결과]

==> 그레들 버전이 5이상인지 5미만인지 확인이 필요함

​ 

  - HelloController에 ResponseDto 사용을 위한 코드 추가

​  - @RequestParam : 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션

                                    외부에서 name(@RequestParam("name")이란 이름으로 넘긴

                                    파라미터를 메소드 파라미터 name(String name)에 저장하게 됨​​

//HelloController.java

package com.jojoldu.book.springboot.web;

import com.jojoldu.book.springboot.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 // 1
public class HelloController {

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

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

 

​  - 추가된 API를 테스트하는 코드를 HelloControllerTest에 추가

​  - param : API 테스트할 때 사용될 요청 파라미터를 설정함 

​                   단, 값은 String만 허용되며, 숫자/날짜 등의 데이터를 등록할 때는 문자열로 변경해야 함

​​  - jsonPath : JSON 응답값을 필드별로 검증할 수 있는 메소드이며, $를 기준으로 필드명을 명시함 ​​​

//HelloControllerTest.java

package com.jojoldu.book.springboot.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.*;
import static org.hamcrest.Matchers.is;
@RunWith(SpringRunner.class) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {

    @Autowired // 3
    private MockMvc mvc; // 4

    @Test 
    public void hello가_리턴된다() throws Exception{
        String hello = "hello";
            // 5                                // 6                       // 7
        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") // 5 
                        .param("name", name)
                        .param("amount",String.valueOf(amount))).andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name)))	 // 6 
                .andExpect(jsonPath("$.amount", is(amount)));  // 7
    }
}​

[dto API 테스트 결과]

728x90
반응형

댓글