[Spring Boot]간단한 테스트 code를 만들어 보자!

윤동환·2022년 12월 28일
0
post-thumbnail

TDD와 단위 테스트의 관계

TDD는 테스트 주도 개발을 의미한다.
즉, test code를 먼저 작성하는 것으로 시작한다.
사진 출처 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 : 이동욱 (책)

단위테스트

단위테스트는 TDD의 첫번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 의미한다.

단위테스트의 장점

■ 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.
■ 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에
서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다(예, 회귀 테스트).
■ 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.
■ 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다.

테스트 코드 xUnit 프레임 워크들

  • JUnit - Java
  • DBUnit - DB
  • CppUnit - C++
  • NUnit - .net

Test 코드 작성

Hello Controller Test code 작성하기

1. 프로젝트내에 패키지 생성

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

2. 패키지 내에 클래스 생성

클래스에 spring boot의 기본 설정을 해주자

package orm.example.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);
    }
}

Application class는 메인 클래스로 사용할 예정입니다.

@SpringBootApplication란?

스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정합니다.

  • @SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 이 class는 항승 프로젝트의 최 상단에 위치 시켜야 합니다.

여기에선 내장 WAS로 사용할 것입니다.
(내장 WAS : 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것)
이유는
언제 어니서나 같은 환경에서 스프링 부트를 배포할수 있기 때문입니다.

Application패키지 내에 web패키지 생성

테스트 해볼 controller 생성하기 위해 class 생성


HelloController class

package orm.example.springboot.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";
    }
}

Annotation설명

  1. @RestController
  • 컨트롤러를 JSON을 반환하는 컨트롤럴로 만들어 줍니다.
  • 예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해줍니다.
  1. @GetMapping
  • HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 줍니다.
  • 예전에는 @RequestMapping(method = RequestMethod.GET)으로 사용되었습니다.

테스트 코드 검증

src/test/java에 패키지 생성

패키지에 test할 controller를 이름 뒤에 Test를 붙여서 생성

package orm.example.springboot;

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 orm.example.springboot.web.HelloController;

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) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {
    @Autowired // 3
    private MockMvc mvc; // 4
        @Test
        public void hello_returned() throws Exception {
            String hello = "hello";
            mvc.perform(get("/hello")) // 5
                .andExpect(status().isOk()) // 6
                .andExpect(content().string(hello)); // 7
        }
}

코드 및 Annotation설명

  1. @RunWith(SpringRunner.class)
    • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킵니다.
    • 여기서는 SpringRunner라는 스프링 실행자를 사용합니다.
    • 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 합니다.
    사용 이유 : RunWith 대신 SpringBoot를 사용하면 application context를 전부 로딩하여 자칫 무거운 프로젝트가 될 수 있음. Junit4에서 필요한 조건에 맞춰서 사용하기 위함

  2. @WebMvcTest
    • 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션 입니다.
    • 선언할 경우 @Controller, @ControllerAdvice 등을 사용할 수 있습니다.
    • 단, @Service, @Component, @Repository 등은 사용할 수 없습니다. (여기서는 컨트롤러만 사용하기 때문에 선언합니다.)

  3. @Autowired
    • 스프링이 관리하는 빈(Bean)을 주입 받습니다.

  4. private MockMvc mvc
    • 웹 API를 테스트할 때 사용합니다.
    • 스프링 MVC 테스트의 시작점입니다.
    •이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.

  5. mvc.perform(get("/hello"))
    MockMvc를 통해 /hello 주소로 HTTP GET 요청을 합니다.
    • 체이닝이지원되어 아래와같이 여러 검증기능을 이어서 선언할 수 있습니다.

  6. andExpect(status( ).isOk( ))
    • mvc.perform의 결과를 검증합니다.
    • HTTP Header의 Status를 검증합니다.
    • 우리가 흔히 알고 있는 200, 404, 500 등의 상태를 검증합니다. (여기선 OK 즉, 200인지 아닌지를 검증합니다.)

  7. andExpect(content( ).string(hello))
    • mvc.perform의 결과를 검증합니다.
    • 응답 본문의 내용을 검증합니다.
    • Controller에서 “hello”를 리턴하기 때문에 이 값이 맞는지 검증합니다.

import org.junit.Test;
import org.junit.runner.RunWith;
@WebMvcTest(controllers = HelloController.class) 에 에러가 발생했습니다.
Testing과 Junit4를 경로에 추가해주고
import orm.example.springboot.web.HelloController;
를 추가해주어 문제를 해결하였습니다.

작성한 테스트 코드를 실행시켜보았습니다.


테스트가 통과된 것을 확인 할 수 있습니다.

기존에 작성한 class를 실제로 실행

기존에 작성한 application.java 파일에서 실행을 시켜보면 tomcat 서버가 8080으로 실행된 것을 확인 할 수 있습니다.

localhost:8080으로 그냥 요청하게 되면
이렇게 404 에러가 발생하며 요청한 api를 찾을 수 없다고 나옵니다. 저는 hello라는 url에 맞는 api만 생성하였기 떄문에 /hello까지 입력을 해주어야 합니다.

개발시 중요한 마음가짐
수동으로 검증후 테스트 코드를 작성하지 않는다.
테스트 코드 먼저 검증 후, 정만 못 믿겠을 때 프로젝트를 실행하여 확인해야 한다.

적용한 코드를 Lobbok으로 전환하기

HelloResponseDto를 생성

//src/main/java/com/jojoldu/book/springboot/web/dto/
package orm.example.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter // 1
@RequiredArgsConstructor // 2
public class HelloResponseDto {
    private final String name;
    private final int amount;
}
  1. @Getter
  • 선언된 모든 필드의 get 메소드를 생성해 줍니다.
  1. @RequiredArgsConstructor
  • 선언된 모든 final 필드가 포함된 생성자를 생성해 줍니다.
  • final이 없는 필드는 생성자에 포함되지 않습니다.

Dto에 적용된 Lombok이 잘 작동하는지 테스트

package orm.example.springboot.dto;

import org.junit.Test;
import orm.example.springboot.web.dto.HelloResponseDto;

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
        //test용 name과 amount를 초기화하고 name과 amount를 인자로 dto 객체를 생성해 현재 지역 변수인 name과 amount와 같은지 testing하는 코드
        assertThat(dto.getName()).isEqualTo(name); // 1, 2
        assertThat(dto.getAmount()).isEqualTo(amount);
    }
}
  1. assertThat
    • assertj라는 테스트 검증 라이브러리의 검증 메소드입니다.
    • 검증하고 싶은 대상을 메소드 인자로 받습니다.
    • 메소드 체이닝이 지원되어 isEqualTo와 같이 메소드를 이어서 사용할 수 있습니다.
  2. isEqualTo
    • assertj의 동등 비교 메소드입니다.
    • assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공입니다.

Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용한 이유
1. CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않습니다.
-> Junit의 assertThat을 쓰게 되면 is( )와 같이 CoreMatchers 라이브러리가 필요합 니다.
2. 자동완성이 좀 더 확실하게 지원됩니다.
-> IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약합니다.

테스트 실행

에러 발생시 이 글을 참고하세요

테스트 결과
테스트 성공으로 HelloResponseDto에 getter와 생성자를 따로 선언해두지 않았음에도 @Getter로 get 메소드가, @RequiredArgsConstructor로 생성자가 자동으로 생성되는 것이 증명되었습니다.

package orm.example.springboot.web.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

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

HelloController에 ResponseDto를 사용하도록 수정

package orm.example.springboot.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import orm.example.springboot.web.dto.HelloResponseDto;

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

    @GetMapping("/Hello/dto")
    public HelloResponseDto helloDto(
            @RequestParam("name") String name, //외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
            @RequestParam("amount") int amount) {
        return new HelloResponseDto(name, amount);
    }
}
  1. @RequestParam
    • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션입니다.
    • 여기서는 외부에서 name (@RequestParam("name")) 이란 이름으로 넘긴 파라미터를 메소드 파라미터 name(String name)에 저장하게 됩니다.

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

package orm.example.springboot;

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 orm.example.springboot.web.HelloController;

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;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

@RunWith(SpringRunner.class) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {
    @Autowired // 3
    private MockMvc mvc; // 4
        @Test
        public void hello_returned() throws Exception {
            String hello = "hello";
            mvc.perform(get("/hello")) // 5
                .andExpect(status().isOk()) // 6
                .andExpect(content().string(hello)); // 7
        }

        @Test
        public void helloDto_returned() throws Exception {
            String name = "hello";
            int amount = 1000;

            mvc.perform(get("hello/dto")
		            //8
                    .param("name", name).param("amount", String.valueOf(amount)))
             		//9
                    .andExpect(status().isOk()).andExpect(jsonPath("$.name", is(name))).andExpect(jsonPath("$.amount", is(amount)));
        }
}
  1. param
    • API 테스트할 때 사용될 요청 파라미터를 설정합니다.
    • 단, 값은 String만 허용됩니다. -> amount를 string으로 바꾼 이유
    • 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다.

  2. jsonPath
    • JSON 응답값을 필드별로 검증할 수 있는 메소드입니다.
    • $를 기준으로 필드명을 명시합니다.
    • 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증합니다.

테스트 결과


처음엔 실패했는데 이유는 HelloController에 api경로를 /hello/dto가 아닌 /Hello/dto로 오타가 있었다...

profile
모르면 공부하고 알게되면 공유하는 개발자

0개의 댓글