02 스프링 부트에서 테스트 코드를 작성하자

vencott·2021년 6월 2일
0

견고한 서비스를 만들기 위해선 테스트 코드 / TDD가 매우 중요하다

최근 대부분의 회사에서 테스트 코드에 관해 요구하고 있으며 단위 테스트에 대한 경험을 필수조건으로 둔다

이번 장에선 테스트 코드 작성의 기본을 알아볼 것이다

2.1 테스트 코드 소개

TDD와 단위 테스트

TDD

  • 테스트가 주도하는 개발
  • 개발 시 테스트 코드를 먼저 작성하는 것부터 시작

  • RED: 항상 실패하는 테스트를 먼저 작성하고
  • GREEN: 테스트가 통과하는 프로덕션 코드를 작성하고
  • REFACTOR: 테스트가 통과하면 프로덕션 코드를 리팩토링합니다

단위 테스트

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

이번 장에선 단위 테스트 코드를 학습하며, 추후 TDD를 학습하길 추천한다

왜 테스트 코드를 작성 해야할까?

단위 테스트 코드 작성의 이점은 다음과 같다(위키피디아)

  • 개발단계 초기에 문제를 발견하게 도와준다
  • 코드 리팩토링 / 라이브러리 업그레이드 등의 상황에서 기존 기능이 올바르게 작동하는 지 확인할 수 있다
  • 기능에 대한 불확실성을 감소시킬 수 있다
  • 단위 테스트 자체가 문서로 활용될 수 있다

조금 더 실용적인 이점은 다음과 같다

1) 빠른 피드백

단위 테스트 이전의 개발 프로세스는 다음과 같았다

  1. 코드 작성
  2. 프로그램(Tomcat) 실행
  3. Postman과 같은 API 테스트 도구로 HTTP 요청
  4. 요청 결과를 System.out.println()으로 눈으로 검증
  5. 결과가 다르면 다시 프로그램(Tomcat)을 중지하고 코드 수정

여기서, 2~5의 과정은 코드가 수정될 때 마다 반복하는 작업이며, 특히 톰캣을 내렸다가 다시 실행하는 것은 많은 시간이 소요된다

테스트 코드가 없다 보니 눈과 손으로 직접 수정된 기능을 확인할 수 밖에 없어서 생긴 문제이다

2) 자동검증

System.out.println()을 통해 눈으로 검증하다 보니, 놓치는 부분이 있을 수 있다

테스트 코드를 작성한다면 자동검증이 가능하다

3) 개발자가 만든 기능을 안전하게 보호

시스템에 B라는 기능을 추가 할 때, 기존에 잘 되던 A라는 기능이 안되는 경우가 발생한다

새로운 기능을 추가할 때마다 서비스의 모든 기능을 테스트 하기엔 자원과 시간이 너무 많이 든다

테스트 코드를 구현해 놓는다면, 새로운 기능이 추가 될 때마다 테스트 코드를 수행하기만 하면 되므로 테스트 코드는 기존 기능이 잘 작동하는 것을 보장해 준다

테스트 코드 프레임워크

가장 대중적인 테스트 프레임워크는 xUnit이다

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

  • JUnit - Java
  • DBUnit - DB
  • NUInit - .Net

이 책에서는 JUnit4를 사용한다

2.2 Hello Controller 테스트 코드 작성하기

1장에서 만든 프로젝트에서 패키지를 생성한다

src → main → java → 마우스 우클릭 → New → Package

패키지명은 일반적으로 웹사이트 주소와 반대로 명명한다

1장에서 사용한 Group ID인 com.vencott.dev에 현재 프로젝트명인 springboot를 덧붙인 com.vencott.dev.springboot로 생성한다

그 다음, Java 클래스를 생성한다

패키지 → 마우스 우클릭 → New → Java class

Application 클래스

클래스의 이름은 Application으로 하고 다음과 같이 코드를 작성한다

src/main/java/com/vencott/dev/springboot/Application.java

package com.vencott.dev.springboot;

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

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

Application 클래스는 프로젝트의 메인 클래스이다

  1. @SpringBootApplication
    • 스프링 부트의 자동설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정한다
    • 어노테이션의 위치부터 설정을 읽어가므로 항상 프로젝트의 최상단에 위치
  2. SpringApplication.run()
    • 내장 WAS를 실행
    • 내장 WAS를 사용하면 항상 서버에 톰캣을 설치할 필요가 없고, 스프링 부트로 만들어진 Jar 파일로 실행
    • 스프링 부트에서는 내장 WAS를 사용하는 것을 권장한다 → 언제 어디서나 같은 환경에서 스프링 부트 배포가 가능하기 때문

테스트 대상 Controller 생성

먼저, 최상위 패키지 안에 web 패키지를 생성한다

  • 앞으로 컨트롤러와 관련된 클래스들은 모두 이 패키지에 담는다

HelloController라는 이름으로 컨트롤러를 하나 생성한다

src/main/java/com/vencott/dev/springboot/web/HelloController.java

package com.vencott.dev.springboot.web;

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

@RestController // 1
public class HelloController {

    @GetMapping("/hello") // 2
    public String hello() {
        return "hello";
    }
}
  • (1) @RestController
    • JSON을 반환하는 컨트롤러로 만들어 준다
    • 이전에 @RequsetBody를 각 메소드마다 선언했던 것을 한번에 해준다고 생각한다
  • (2) @GetMapping
    • HTTP Get 메소드를 받을수 있는 API를 만들어 준다
    • 이전에 @RequsetMapping(method = RequestMethod.GET)과 같은 기능

테스트 코드 작성

이제, 테스트 코드로 검증할 차례이다

  1. src/test/java 디렉토리에 앞에서 작성한 패키지와 동일한 패키지를 생성한다
  2. 대상 클래스 이름에 Test를 붙여 테스트 클래스를 생성한다

src/test/java/com/vencott/dev/springboot/web/HelloControllerTest.java

package com.vencott.dev.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.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가_리턴된다() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello")) // 5
                .andExpect(status().isOk()) // 6
                .andExpect(content().string(hello)); // 7
    }
}
  1. @RunWith(SpringRunner.class)
    • 스프링 부트 테스트와 Junit 사이에 연결자 역할
    • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행
  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
  7. .andExpect(content().string(hello))
    • mvc.perform()의 결과를 검증
    • 응답 본문의 내용을 검증한다
    • Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 확인한다

테스트 코드 실행

메소드 왼쪽의 화살표를 클릭한다

다음과 같이 테스트가 통과한 것을 확인할 수 있다

우리가 검증하고자 했던.andExpect(status().isOk()).andExpect(content().string(hello)) 가 모두 테스트를 통과한 것을 의미한다

수동으로 확인하기

테스트 코드가 아닌 수동으로 앱을 실행시켜 결과를 확인해본다

Application.java 파일로 이동해 main 메소드의 왼쪽 화살표 버튼을 클릭한 후, Run 'Application.main()' 버튼을 클릭한다

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

웹 브라우저에서 localhost:8080/hello로 접속해 테스트 코드와 결과가 같은 것을 확인한다

테스트 코드 vs 수동 확인

절대 수동으로 검증하고 테스트 코드를 작성하지 않는다

테스트 코드로 먼저 검증 후, 다시 한번 확인할 때 프로젝트를 실행해 확인하는 순서를 잊지 말아야 한다

테스트 코드를 꼭 작성해야 견고한 소프트웨어를 만드는 역량이 성장할 수 있다

2.3 롬복 소개 및 설치하기

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

인텔리제이는 플러그인 덕분에 쉽게 롬복을 사용할 수 있다

프로젝트에 롬복을 추가

build.gradle에 의존성을 추가한 후 Refresh한다

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

Plugin에서 Lombok을 설치한다

Settings > Build > Compiler > Annotation Processors에서 Enable annotation processing 옵션을 체크한다

2.4 Hello Controller 코드를 롬복으로 전환하기

기존 코드를 롬복으로 변경하는 일은 규모가 큰 프로젝트면 쉽지 않은 일이다

어떤 기능이 제대로 작동될지 안 될지 예측하기 어렵기 때문이다

하지만 테스트 코드를 잘 작성했다면 롬복으로 전환하고 테스트 코드만 돌려보면 된다

Dto 생성

web 패키지에 dto 패키지를 추가한다

앞으로 모든 응답 Dto는 이 Dto 패키지에 추가한다

src/main/java/com/vencott/dev/springboot/web/dto/HelloResponseDto.java

package com.vencott.dev.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 메소드를 생성해준다
  2. @RequiredArgsConstructor
    • 선언된 모든 final 필드가 포함된 생성자를 생성해준다
    • final이 없는 필드는 생성자에 포함되지 않는다

Dto 테스트

Dto에 적용된 롬복이 잘 작동하는지 테스트 코드를 작성한다

src/test/java/com/vencott/dev/springboot/web/dto/HelloResponseDtoTest.java

package com.vencott.dev.springboot.web.dto;

import org.junit.Test;

import static org.assertj.core.api.AssertionsForClassTypes.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); // 1, 2
        assertThat(dto.getAmount()).isEqualTo(amount);
    }

}
  1. assertThat
    • assertj라는 테스트 검증 라이브러리의 메소드
      • Junit의 기본 assertThat이 아닌 assertjassertThat 로 다음과 같은 장점이 있다
        • CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다
        • 자동완성이 좀 더 확실하게 지원된다
    • 검증하고 싶은 대상을 메소드 인자로 받는다
    • 체이닝을 통해 isEqualTo와 같이 메소드를 이어서 사용할 수 있다
  2. isEqualTo
    • assertj의 동등 비교 메소드
    • assetThat에 있는 값과 isEqualTo의 값을 비교해 같을 때만 성공한다

테스트를 실행해 보면 정상적으로 수행됨을 확인할 수 있다

롬복의 @Getter로 get 메소드가, @RequiredArgsConstructor로 생성자가 자동으로 생성되는 것이 증명된 것이다

HelloController에서 ResponseDto 사용

먼저 HelloController에 새로 만든 ResponseDto를 사용하도록 코드를 추가한다

src/main/java/com/vencott/dev/springboot/web/HelloController.java

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

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

src/test/java/com/vencott/dev/springboot/web/HelloControllerTest.java

@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))) // 1
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name))) // 2
                .andExpect(jsonPath("$.amount", is(amount)));

    }
  1. param
    • API 테스트 시 사용될 요청 파라미터를 설정
    • 값은 String만 허용(숫자나 날짜 등의 데이터도 문자열로 변경)
  2. jsonPath
    • JSON 응답값을 필드별로 검증할 수 있는 메소드
    • $를 기준으로 필드명을 명시

테스트 결과, JSON이 리턴되는 API 역시 정상적으로 통과됨을 확인한다


출처: 이동욱 저, 『스프링 부트와 AWS로 혼자 구현하는 웹 서비스』, 프리렉(2019)


profile
Backend Developer

0개의 댓글

관련 채용 정보