스프링 부트 테스트 코드 작성 - 루타블의 개발일기

김주영·2022년 7월 4일
0
post-thumbnail

[본 글은 프로젝트 과정을 기록할 목적으로 작성되었으며 아래 교재에 기반하여 작성됨]

🌱 TDD(Test-Driven Development)


  • 항상 실패하는 테스트를 먼저 작성하고(Red)
  • 테스트가 통과하는 프로덕션 코드를 작성하고(Green)
  • 테스트가 통과하면 프로덕션 코드를 리팩토링한다.(Refactor)

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

기능 단위로 기대하는 결과가 나오는지 테스트하는 코드

🌿 테스트 코드 작성 이점

  1. 빠른 피드백

    코드를 수정할 때마다 계속 톰캣을 내렸다가 다시 실행하는 반복 작업을 하지 않아도 된다.

  2. 자동 검증

    println()으로 확인하지 않아도 작성된 단위 테스트를 실행만 하면 검증이 가능하다.

  3. 개발자가 만든 기능을 안전하게 보호해 준다.

    새로운 기능을 추가했을 때, 기존 기능이 잘 작동되는 것을 보장해 준다. 즉, 기존 기능과 새로운 기능을 함께 검증할 수 있는 테스트 코드를 작성해 둔다면 문제를 조기에 찾을 수 있다.

  • 가장 대중적인 테스트 프레임워크인 xUnit 중에서 자바용인 JUnit을 사용하겠다.

🌱 Annotations


📢 @SpringBootApplication

스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정. 그리고 @SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 이 클래스는 항상 프로젝트의 최상단에 위치해야만 한다.

📢 @RestController

@ResponseBody + @Controller 라고 보면 될 것이라 생각한다.

보통 컨트롤러는 return 값에 view 이름을 넣어 렌더링할 화면 이름이 매핑되도록 한다. 하지만 @ResponseBody는 View 조회를 무시하고 HTTP body에 반환 값을 직접 등록할 수 있다. Java의 객체 데이터를 프론트단에 전달하기 위해서는 Json 형태로 전달해야 하는데 이를 도와주는 애노테이션이다.

@Controller는 해당 클래스가 컨트롤러임을 명시하면서 컴포넌트 스캔 대상이 되므로 Spring Bean으로 자동 등록된다.

📢 @GetMapping

Http Method인 Get 요청을 받을 수 있는 API를 만들어 준다.

📢 @RunWith(SpringRunner.class)

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

JUnit4에서 지원

JUnit 5.x부터 @ExtendWith(SpringExtension.class)로 대체됨.
@ExtendWith은 메타 애노테이션을 지원하므로 @SpringBootTest를 사용한다면 다시 선언하지 않아도 된다.

@SpringBootTest를 사용하면 Application Context(스프링 컨테이너) 전체를 로딩하므로 통합 테스트에는 용이하지만 시간이 오래 걸리고 무겁다. 따라서, JUnit의 기본 러너 대신 사용할 러너를 지정하여 테스트를 실행시키도록 지시할 수 있다. 이렇게 하는 이유는 필요한 것만 스프링 컨테이너에 로딩하여 좀 더 가볍게 테스트를 수행하기 위함이다.

📢 @WebMvcTest

웹(MVC) 테스트에 특화된 애노테이션이다.

Application Context를 완전하게 구동하지 않고 web layer를 테스트하
고 싶을 때 사용을 고려한다.
@SpringBootTest의 경우, 모든 빈을 로드하기 때문에 테스트 구동 시간이 오래 걸리고, 테스트 단위가 크기 때문에 디버깅이 어려울 수 있다. 이를 위해 특정 Controller만 슬라이스해서 가볍게 테스트하고 싶을 때 유용하다.
@Controller, @ControllerAdvice 등은 사용할 수 있지만 @Service, @Component, @Repository 등은 사용할 수 없다. 따라서, Service, Repository dependency가 필요한 경우에는 @MockBean으로 주입받아 테스트를 진행한다.
ref : https://gom20.tistory.com/123

🌱 Controller 테스트


🔧 java 디렉토리 아래 최상위 패지키를 추가하겠다. 일반적으로 패키지명은 웹 사이트 주소의 역순으로 한다. 앞서 Group Id에 프로젝트명을 딴 'com.bbs.projects.bulletinboard'로 하겠다.

🔧 프로젝트 최상단에서 메인 쓰레드를 실행할 Application이라는 클래스를 추가한다. 해당 위치부터 설정을 읽어갈 것이기 때문에 앞서 설명한대로 @SpringBootApplication을 추가한다.

package com.bbs.projects.bulletinboard;

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); //내장 WAS 실행
    }
}

🌿 내장 WAS(Web Application Server)

main 메소드의 SpringApplication.run으로 인해 실행

별도로 외부에 두지 않고 내부에서 WAS를 실행하는 것으로, 항상 서버에 톰캣(Tomcat)을 설치할 필요가 없게 되고, 스프링 부트로 만들어진 Jar파일(실행 가능한 Java 패키징 파일)로 실행하면 된다.

외장 WAS는 모든 서버의 WAS의 종류와 버전, 설정을 일일이 일치시켜야 하므로 서버 대수가 늘어나면 굉장히 번거럽고 큰 작업이 될 것이다. 따라서, 스프링 부트에서는 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있는 내장 WAS 사용을 권장하고 있다.

🔧 앞으로 모든 컨트롤러는 web 패키지에 담도록 함

🌿 HelloController


package com.bbs.projects.bulletinboard.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
    	//HTTP 메시지 바디에 직접 데이터를 write
        return "hello";
    }
}

HelloController는 "/hello"가 붙은 URL로 Get 요청(조회 요청)이 들어올 경우 hello()를 실행하고, @RestController에 의해 Http 응답 바디에 "hello"가 전달되어 브라우저에 나타난다.
ex) localhost:8080/hello

🌿 HelloControllerTest

package com.bbs.projects.bulletinboard.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.junit.Assert.*;
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; //Web Api 환경 제공

    @Test
    public void return_hello() throws Exception {
        String hello = "hello";

		//상태 코드와 메시지 바디 검증
        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }

}

MockMvc는 웹 API를 테스트할 때 사용한다. 즉, 실제 웹 애플리케이션을 WAS에 배포하지 않고 MVC 환경에서 요청/전송/응답 기능을 제공한다. 스프링 테스트의 시작점이라 할 수 있다.

"/hello" URL로 Get 요청을 한 후에 isOK(200) 상태 코드와 응답 본문의 내용(Http 응답 바디)을 검증한다.

🌱 Lombok 추가


Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해 주는 자바 개발자들의 필수 라이브러리

🔧 settings -> Annotation Processors -> Enable annotation processing 체크

🔧 Marketplace에서 lombok 검색 -> 설치

🌿 build.gradle

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

🌿 HelloController를 롬복으로 전환

🔧 web 패키지에 dto 패키지 추가

🔧 모든 응답 Dto를 dto 패키지에 추가할 것임

🌳 HelloResponseDto

package com.bbs.projects.bulletinboard.web.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class HelloResponseDto {

	//불변 필드+생성자 주입
    private final String name;
    private final int amount;

}

@Getter는 자동으로 get메소드를 생성해 준다.

@RequiredArgsConstructor는 선언된 모든 final 필드가 포함된 생성자를 생성해 준다. 장점은 final을 사용할 수 있다는 것과 @Autowired는 생성자가 클래스 내 하나만 존재하는 경우 생략이 가능하기 때문에 @Autowired를 생성자에 달아준 생성자 주입 효과를 얻을 수 있다.

🌳 HelloResponseDtoTest

package com.bbs.projects.bulletinboard.web.dto;

import org.junit.Test;
import static org.assertj.core.api.Assertions.*;

public class HelloResponseDtoTest {

    @Test
    public void test_lombok() throws Exception{
        //given
        String name = "test";
        int amount = 1000;

        //when
        //name과 amount를 전달 케이스(DTO)에 담음
        HelloResponseDto dto = new HelloResponseDto(name, amount);

        //then
        assertThat(dto.getName()).isEqualTo(name);
        assertThat(dto.getAmount()).isEqualTo(amount);
    }

}

Assertions 라이브러리의 assertThat을 이용하면 해당 값이 기대한 값과 동일한지 검증할 수 있다.

🌳 HelloController에서 ResponseDto 사용

package com.bbs.projects.bulletinboard.web;

import com.bbs.projects.bulletinboard.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")
    //URL 상에서 해당 파라미터를 찾아서 DTO에 넣은 DTO 객체 반환
    public HelloResponseDto helloDto(@RequestParam("name") String name,
                                     @RequestParam("amount") int amount) {

        return new HelloResponseDto(name, amount);

    }
}

@RequestParam을 통해 외부에서 API로 넘긴 파라미터를 가져올 수 있다.

🌳 HelloControllerTest에서 추가된 API 테스트

package com.bbs.projects.bulletinboard.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.*;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc; //Web Api 환경 제공

    @Test
    public void return_hello() throws Exception {
        String hello = "hello";

		//상태 코드와 메시지 바디 검증
        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }

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

        //JSON 필드별 검증
        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)));
    }

}

MockMvc로 API 환경에서 테스트를 수행하기 때문에 .param을 사용하여 요청 파라미터를 설정한다. 단, 값은 String만 허용된다.

앞의 테스트와 마찬가지로, .andExpect를 통해 상태 코드와 필드 값을 검증할 수 있다. 여기서 jsonPath는 JSON 응답값을 필드별로 검증할 수 있는 메소드이다.
ex) "$.필드명"

🔍 HTTP 상의 객체 전송은 JSON 형태임을 잊지 말자!

📝 참고

DAO(Data Access Object)

  • DB의 데이터에 접근하기 위한 객체
  • DB 접근용 로직과 비즈니스 로직을 분리하기 위해 사용

DTO(Data Transfer Object)

  • 계층 간 데이터 교환을 하기 위한 객체
  • getter&setter만 가진 로직이 없는 순수한 객체
  • 물건을 보관하고 전달할 때 사용하는 케이스라고 생각해도 될 것 같다.

ex) form을 통해 넣은 데이터는 DTO에 넣어서 전송
ex) DTO를 받은 서버는 DAO를 이용하여 DB로 데이터를 넣음

0개의 댓글