책과 김영한님의 inflern에서의 강의 모두 테스트 코드의 작성을 강조합니다. 모든 프로젝트를 작성할 때 테스트 코드는 선택이 아닌 필수인 것 같습니다. 저는 스프링 공부를 해보며 Junit을 통해 테스트 코드를 작성해봤는데, 실제 무거운 프로그램을 돌리며 테스트를 하는 시간을 줄일 수 있어서 편했던 기억이 있습니다.
이번 시간에는 앞으로 진행할 프로젝트에서 가장 중요한 테스트 코드 작성의 기본을 배워보고자 합니다.
먼저 한가지 짚고 갈 것은 TDD와 단위 테스트는 다른 이야기 입니다. TDD는 테스트가 주도하는 개발을 이야기하고, 테스트 코드는 먼저 작성하는 것 부터 시작합니다.
TDD
- 항상 실패하는 테스트를 먼저 작성하고 (Red)
- 테스트가 통과하는 프로덕션 코드를 작성하고 (Green)
- 테스트가 통과하면 프로덕션 코드를 리팩토링합니다. (Refactor)
반면, 단위 테스트는 TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 의미합니다. 즉, 순수하게 테스트 코드 작성하는 것을 이야기 합니다. 제가 배울 것은 TDD가 아닌 단위 테스트 코드를 배웁니다. 이번 기회를 통해 테스트 코드를 먼저 배우고 따로 TDD를 공부해야봐야할 것 같습니다.
그렇다면 테스트 코드는 왜 작성해야 할까요?
위키피디아에서는 아래와 같이 이야기합니다.
- 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.
- 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다. (예, 회귀 테스트)
- 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.
- 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다.
여기에 책을 쓰신 이동욱님은 추가적으로 가장 먼저 빠른 피드백이 있다고 말하였습니다. 단위 테스트를 배우기 전에 진행한 개발 방식은 아래와 같습니다.
- 코드 작성
- 프로그램(Tomcat) 실행
- Postman과 같은 API 테스트 도구로 HTTP 요청
- 요청 결과를 System.out.println()으로 눈으로 검증
- 결과가 다르면 다시 프로그램(Tomcat)을 중지한 뒤 코드 수정
여기서 2~5는 매번 코드를 수정할 때마다 반복해야되는 부분입니다. 톰캣을 재시작하는 시간은 1분 이상까지 소요되기도하며, 수십번씩 수정해야하는 상황에서 아무런 코드 작업 없이 1시간 이상 소요되기도 했다고 합니다.
테스트 코드를 작성하면 좋은 첫번째는 위의 문제가 해결됩니다.
두번째는 System.out.println()으로 직접 보지않고, 자동검증이 가능합니다. (Junit을 통해 테스트가 정상적으로 완료되면 프로그램 테스트는 성공적으로 완료됩니다.)
세번째는 개발자가 만든 기능을 안전하게 보호해줍니다. 예를들어, B라는 기능이 추가되어 테스트를 한다고 가정해봅시다. B 기능이 잘 되어 오픈했더니 기존에 잘되던 A 기능에 문제가 생긴 것을 발견합니다. 이런 문제는 규모가 큰 서비스에서는 빈번하게 발생하는 일이라고 합니다. 하나의 기능을 추가할 때마다 너무나 많은 자원이 들기 때문에 서비스의 모든 기능을 테스트할 수 는 없습니다.
이렇게 새로운 기능이 추가될 때, 기존 기능이 잘 작동되는 것을 보장해주는 것이 테스트 코드입니다. A라는 기존 기능에 기본 기능을 비롯해 여러 경우를 모두 테스트 코드로 구현해 놓았다면 테스트 코드를 수행만하면 문제를 조기에 찾을 수 있습니다.
서비스 기업에서는 특히나 강조되고 있어 이동욱님은 100% 익혀야 할 기술이자 습관이라고 합니다.
대표적인 테스트 프레임워크 xUnit
- JUnit - JAVA
- DBUnit - DB
- CppUnit - C++
- NUnit - .net
이 중에서 저는 JUnit을 앞으로 사용할 것 입니다. 또한, 많은 회사에서 사용중인 JUnit4로 작성하도록 하겠습니다.
Hello Controller 테스트 코드 작성하기
일반적으로 패키지명은 웹 사이트 주소의 역순으로 한다고 합니다. 저는 com.qazyj.book.springboot라고 패키지명을 생성(main, test)해준 뒤, application이라는 클래스를 패키지안에 생성해주었습니다. 클래스 안에 psvm(public static void main)을 쳐서 자동완성 해준 뒤, @SpringBootApplication 어노테이션을 추가해주었습니다. 코드는 아래와 같습니다.
package com.qazyj.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);
}
}
- @SpringBootApplication : 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정됩니다. 특히, 해당 어노테이션이 있는 위치부터 설정을 읽어가기 때문에 해당 어노테이션으로 선언된 클래스는 항상 프로젝트 최상단에 위치해야 합니다.
해당 psvm 메소드안에 실행하는 SpringApplication.run으로 인해 내장 WAS (Web Application Server, 웹 애플리케이션 서버)를 실행합니다. 내장 WAS란 별도의 외부 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 말합니다. 이렇게 되면 항상 서버에 톰캣을 설치할 필요가 없게 되고, 스프링 부트로 만들어진 Jar 파일로 실행하면 됩니다. 스프링 부트는 내장 WAS를 사용하는 것을 권장하고 있습니다. 이유는 "언제 어디서나 같은 환경에서 스프링 부트를 배포"할 수 있기 때문입니다.
이제 테스트를 위한 Controller를 만들어 보겠습니다.
com.qazyj.book.springboot에 하위 패키지로 web(컨트롤러와 관련된 클래스들을 넣을 패키지)을 생성한 뒤, HelloController라는 클래스를 해당 패키지에 생성해주었습니다. 클래스안에는 간단한 API를 만들어 보겠습니다. 코드는 아래와 같습니다.
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";
}
}
- @RestContoller : 컨트롤러를 JSON으로 반환하는 컨트롤러로 만들어 줍니다.
- @GetMapping : HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 줍니다.
작성한 코드를 테스트하기 위해 src/test/java에 생성한 com.qazyj.book.springboot 패키지 안에 똑같이 web패키지와 HelloControllerTest라는 클래스를 생성해 줍니다. 참고로 테스트 클래스 명은 보통 대상 클래스 이름에 Test를 붙입니다. 클래스 안에 아래와 같이 코드를 작성합니다.
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.*;
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void hello가_리턴된다() throws Exception {
String hello = "hello";
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
}
- @RunWith(SpringRunner.class)
- 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킵니다.
- 여기서는 SpringRunner라는 스프링 실행자를 사용합니다.
- 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 합니다.
- @WebMvcTest
- 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션 입니다.
- 선언할 경우 @Conroller, @ControllerAdvice 등을 사용할 수 있습니다.
- 단, @Service, @Component, @Repository 등은 사용할 수 없습니다.
- 여기서는 컨트롤러만 사용하기 때문에 선언했습니다.
- @Autowired
- 스프링이 관리하는 빈(Bean)을 주입받습니다.
- 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 등의 상태를 검증하는 것이라고 보면됩니다.
- 여기선 200(OK)을 확인합니다.
- .andExpect(content().string(hello))
- mvc.perform의 결과를 검증합니다.
- 응답 본문의 내용을 검증합니다.
- Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증합니다.
테스트 메소드 왼쪽의 화살표를 클릭하면 테스트를 실행할 수 있습니다. 정상적으로 초록색 체크가 뜨면 테스트 상 문제가 없는 것 입니다.
실제 프로젝트를 실행해보고 싶으면 main의 psvm 메서드를 실행한 뒤, 아래와 같이 웹 사이트에서 localhost:8080/hello를 입력하면 아래와 같이 hello가 출력되는 것을 확인할 수 있습니다. (port 번호는 8080이 기본이고, 8080 port번호를 사용해서 에러가 뜨는 분들은 포트 번호를 바꾸시면 됩니다.)
롬복 소개 및 설치하기
다음으로 해볼 것은 자바 개발자들의 필수 라이브러리 롬복입니다.
롬복은 자바 개발할 때 자주 사용하는 코드인 Getter, Setter, 기본생성자, toString 등을 어노테이션으로 자동생성해줍니다.
build.gradle에 dependencies에 아래 코드를 추가해줍시다.
implementation('org.projectlombok:lombok')
추가한 뒤, 코끼리 버튼을 눌러야 적용됩니다.
라이브러리를 다 받았다면 롬복 플러그인을 설치합시다. 윈도우 [컨트롤+시프트+A], 맥[커맨드+시프트+A]를 누른뒤, plugins action을 선택하여 플러그인 설치 팝업이 나오는데 marketplace탭으로 이동하여 Lombok을 검색한 뒤, install 버튼을 눌러 설치해줍시다.
HelloController 코드를 롬복으로 전환하기
기존 코드를 롬복으로 변경해보겠습니다. 우리는 테스트 코드가 우리의 코드를 지켜주기 때문에 쉽게 변경할 수 있습니다. 롬복으로 변경한 뒤, 문제가 생기는지 테스트 코드만 돌려보면 되기 때문입니다. 이처럼 테스트 코드는 중요합니다.
먼저, web 패키지에 dto 패키지를 추가한 뒤, HelloResponseDTO를 생성합니다. 코드는 아래와 같습니다.
@Getter
@RequiredArgsConstructor
public class HelloResponseDTO {
private final String name;
private final int amount;
}
- @Getter
- 선언된 모든 필드의 get 메소드를 생성해줍니다.
- 지금 보이지는 않지만, 필드네이밍에 따라 getName, getAmount라는 메소드가 생성된 것 처럼 사용할 수 있습니다.
- @RequiredArgsConstructor
- 선언된 모든 final 필드가 포함된 생성자를 생성해줍니다. (final로 선언한 필드만)
dto에 적용된 롬복이 잘 작동하는지 간단한 테스트 코드를 작성해주겠습니다. 위와 같은 패키지를 test에도 추가한뒤 클래스명 뒤에 Test를 붙여 클래스를 생성해줍니다. 코드는 아래와 같습니다.
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와 같이 메소드를 이어서 사용할 수 있습니다.
- 현재는 두 파라미터가 같은지 확인합니다. 같다면 정상종료, 다르다면 에러가 발생합니다.
- given, when, then
- given : 어떤 자료형이 주어졌을 때,
- when : 어떠한 작업을 하면
- then : 이러한 결과가 있을 것이다.
여기서 Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용했습니다. assertj 역시 Junit에서 자동으로 라이브러리 등록을 해줍니다.
Junit과 비교하여 assertj의 장점은 아래와 같습니다.
- CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않습니다.
- JUnit의 assertThat을 쓰게 되면 is()와 같이 CoreMatchers 라이브러리가 필요합니다.
- 자동완성이 좀 더 확실하게 지원됩니다.
- IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약합니다.
돌려봤을 때, 오류가 발생하는 분들은 아래와 같이 build.gradle을 수정해주시면 됩니다. (gradle 버전에 따라 lombok 추가해주는 방식이 다른 것 같습니다.)
//기존
implementation('org.projectlombok:lombok')
//추가
testImplementation "org.projectlombok:lombok"
annotationProcessor('org.projectlombok:lombok')
testAnnotationProcessor('org.projectlombok:lombok')
위와 같이 추가하면 프로그램이 test코드가 정상적으로 돌아가는 걸 확인할 수 있습니다.
그렇다면, HeeloController에도 새로만든 ResponseDto를 적용해보겠습니다. 코드는 아래와 같습니다.
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam("name") String name,
@RequestParam("amount") int amount) {
return new HelloResponseDto(name, amount);
}
- @RequestParam
- 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션 입니다.
- 여기서는 외부에서 name(@RequestParam("name"))이란 이름으로 넘긴 파라미터를 name(String name)에 저장하게 됩니다.
name과 amount는 API를 호출하는 곳에서 넘겨준 값들입니다. 추가된 API를 테스트 하는 코드를 HelloControllerTest에 추가합니다. 추가된 코드는 아래와 같습니다.
@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)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is(name)))
.andExpect(jsonPath("$.amount", is(amount)));
}
- param
- API를 테스트할 때 사용될 요청 파라미터를 설정합니다.
- 단, 값은 String만 허용됩니다.
- 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다.
- jsonPath
- JSON 응답값을 필드별로 검증할 수 있는 메소드입니다.
- $를 기준으로 필드명을 명시합니다.
- 여기서는 name과 amount를 검증하기 때문에 $.name, $.amount로 검증합니다.
테스트를 작동시키면 정상적으로 테스트가 통과하는 것을 볼 수 있습니다.
출처
'spring > 스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
[Spring] 6. AWS 서버 환경 구축 - AWS EC2 (0) | 2021.12.30 |
---|---|
[Spring] 5. 스프링시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2021.12.29 |
[Spring] 4. 머스테치로 화면 구성하기 (0) | 2021.12.28 |
[Spring] 3. JPA (0) | 2021.12.28 |
[Spring] 1. 프로젝트 생성 및 gradle 설정 (0) | 2021.12.28 |