Peony의 기록 창고 🌼
article thumbnail
반응형

이 글은 이동욱 님의 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책 내용을 정리한 것입니다.

http://www.yes24.com/Product/Goods/83849117

 

테스트 코드 소개

테스트 코드는 현재 웹 서비스에서 매우 중요한 요소이다. 테스트 코드에서 꼭 짚고 넘어가야 하는것은 TDD와 단위테스트(unit test)다.

 

TDD

테스트가 주도하는 개발(Test-Driven-Development, 혹은 Test-First-Development) 을 의미한다.

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

 

단위 테스트

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

 

❓그럼 테스트 코드는 왜 작성해야 할까?

위키피디아에는 다음과 같이 나와있다. 

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

 

테스트 코드의 장점

  1. 톰캣을 띄워서 System.out.println 등으로 확인할 시간을 줄여준다.
    톰캣을 띄우면 크게 몇 분 이상이 소요된다. 만약 테스트가 개발자가 원하는대로 나오지 않으면 다시 톰캣을 재시작해야 한다. 이 과정을 계속하면 크게 몇 시간이 소비되기도 합니다.
  2. 자동검증 이 되게 합니다. 작성된 단위테스트를 실행만 하면 더는 수동검증은 필요가 없다.
  3. 개발자가 만든 기능을 안전하게 보호 해준다.
    코드를 작성하다보면 여러 코드들이 상호간 영향을 주고 받게 된다. 이 과정에서 A 코드를 수정했을 때 B 코드에 영향을 줘서 B 코드에 대한 테스트 케이스가 실패해 기존 코드에 영향이 없도록 수정해 줄 수있습니다.

언어별로 테스트 코드 여러 테스트코드 프레임워크가 있다. 가장 대중적인 테스트 프레임워크로는 xUnit 이 있다.

개발환경(x) 에 따라 Unit. 테스트를 도와주는 도구라고 생각하면 된다.

  • Java - JUnit
  • DB - DBUnit
  • .net - Unit

이제 본격적으로 코드를 작성해보자.

 

HelloController 테스트 코드 작성하기

1장에서 만든 프로젝트로 패키지를 하나 생성한다. Java 디렉토리를 마우스 오른쪽 버튼으로 클릭하여, [New ⇨ Package]를 차례로 선택해서 생성하자.

일반적으로 패키지명은 웹 사이트 주소의 역순으로 한다. 예를 들어, admin.jojoldu.com이라는 사이트라면 패키지명은 com.jojoldu.admin으로 하면 된다. 여기서는 com.jojoldu.book.springboot로 하겠다.

현재 패키지 아래에 Java 클래스를 생성하자. 클래스 이름은 Application 이다.

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

}
  • Application클래스 : 앞으로 만들 프로젝트의 메인 클래스
  • @SpringBootApplication
    • 스프링 부트의 자동 설정.
    • 스프링 Bean 읽기와 생성을 보두 자동으로 설정 해준다.
  • @SpringBootApplication 이 있는 위치부터 설정을 읽어가기 때문에 항상 프로젝트의 최상단에 위치해야 함
  • SpringApplication.run 으로 인해 내장 WAS(웹 어플리케이션 서버)를 실행.
    • 내장 WAS란?
      • 별도로 외부에 WAS를 두지 않고, 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 의미.
      • 항상 서버에 톰캣을 설치할 필요 X. → 스프링 부트로 만들어진 Jar 파일로 실행하면 된다.
      • 내장 WAS를 사용하는 이유 : 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문

 

Test를 위한 Controller 만들기

현재 패키지 하위에 web이란 패키지를 만들자. 앞으로 컨트롤러와 관련된 클래스들은 모두 이 패키지에 담을 예정이다.

패키지 아래에 Java 클래스를 생성하자. 클래스 이름은 HelloController이다.

src/main/java/com/jojoldu/book/springboot/web/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
public class HelloController {

    @GetMapping("/hello")
    public String Hello() {
        return "hello";
    }
}
  • @RestController
    • 컨트롤러를 JSON으로 반환하는 컨트롤러로 만들어준다.
  • @GetMapping("/hello") : HTTP Method인 GET의 요청을 받을 수 있는 API를 만들어준다.

 

다음은 작성한 코드가 제대로 작동하는지 테스트를 해보자. src/test/java에 앞에서 생성했던 패키지를 그대로 다시 생성하고, 테스트 코드를 작성할 클래스를 생성하자. 일반적으로 테스트 클래스는 대상 클래스 이름에 Test를 붙인다. 여기서는 HelloControllerTest로 생성한다.

 

src/test/java/com/jojoldu/book/springboot/web/HelloControllerTest.java

package com.jojoldu.book.springboot.web;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    void hello가_리턴된다() throws Exception {
        //given
        String hello = "hello";

        //when
        mvc.perform(get("/hello"))
                //then
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
}
  • @WebMvcTest(HelloController.class)
    • 여러 스프링 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션.
    • 선언할 경우, @Controller, @ControllerAdvice 등은 사용할 수 있다.
    • 단, @Service, @Component, @Repository등은 사용할 수 없다.
    • 여기서는 컨트롤러만 사용하기 때문에 선언한다.
  • @Autowired : 스프링이 관리하는 빈을 주입 받는다.
  • private MockMvc mvc;
    • 웹 API를 테스트할 때 사용한다.
    • 스프링 MVC 테스트의 시작점
    • 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.
  • 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의 결과를 검증
    • 응답 본문의 내용을 검증
    • Controlelr에서 "hello"를 리턴하기 때문에 값이 맞는지 검증

 

코드를 다 작성했다면, 테스트 코드를 한 번 실행해보자.

 

다음과 같이 테스트가 통과하는 것을 확인할 수 있다. 그럼 이제 수동으로 실행해서 정상적으로 값이 출력되는지 확인해보자.

 

Application.java로 이동해 main 메소드를 실행해보자. 실행이 끝났다면, localhost:8080/hello로 접속해보자. 그럼 다음과 같이 문자열 hello가 잘 노출되는 것을 확인할 수 있다.

테스트 코드와 결과가 같은 것을 확인할 수 있다.

 

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

web 패키지에 Dto 패키지 추가한다. 이 패키지에 HelloResponseDto를 생성하자.

src/main/java/com/jojoldu/book/springboot/web/dto/HelloResponseDto.java

package com.jojoldu.book.springboot.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이 없는 필드는 생성자에 포함 X

 

Dto에 적용된 롬복이 잘 작동하는지 간단한 테스트 코드를 작성해보자.

src/test/java/com/jojoldu/book/springboot/web/dto/HelloResponseDtoTest.java

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

import org.junit.jupiter.api.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);
        assertThat(dto.getAmount()).isEqualTo(amount);
    }
}
  • assertThat
    • assertj라는 테스트 검증 라이브러리의 검증 메소드
    • 검증하고 싶은 대상을 메소드 인자로 받는다.
    • 메소드 체이닝이 지원되어 isEqualTo와 같이 메소드를 이어서 사용할 수 있다.
  • isEqualTo
    • assertj의 동등 비교 메소드
    • assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공!

 

작성된 테스트 메소드를 실행해보자. 정상적으로 기능이 수행되는 것을 확인하면, HelloController에도 새로 만든 ResponseDto를 사용하도록 코드를 추가해보자.

src/main/java/com/jojoldu/book/springboot/web/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
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);
    }
}
  • @RequestParam
    • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
    • 외부에서 @RequestParam("name")이란 이름으로 넘긴 파라미터를 메소드 파라미터 name(String name)에 저장.

 

이제 API를 테스트하는 코드를 HelloControllerTest에 추가해보자.

src/test/java/com/jojoldu/book/springboot/web/HelloControllerTest.java

package com.jojoldu.book.springboot.web;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(HelloController.class) //@RunWith
public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    void hello가_리턴된다() throws Exception {
        ...
    }

    @Test
    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))
        );

    }
}
  • param
    • API 테스트할 때 사용될 요청 파라미터를 설정
    • 단, 값은 String만 허용
      ⇒ 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능
  • jsonPath
    • JSON 응답 값을 필드별로 검증할 수 있는 메서드
    • $를 기준으로 필드명을 명시
    • 여기서는 name과 amount를 검증하니, $.name, $.amount로 검증

 

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

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju