[STUDY] 스프링부트와 AWS로 혼자 구현하는 웹 서비스 (1)
01. 테스트 코드 작성하기
테스트 코드를 작성하기 전에 TDD와 단위 테스트에 대해 알아보자.
먼저 TDD와 단위 테스트는 다른 이야기다.
TDD는 테스트가 주도하는 개발을 이야기 하며, 테스트 코드를 먼저 작성하는 것부터 시작한다.
- RED: 항상 실패하는 테스트를 먼저 작성하고 시작
- GREEN: 테스트가 통과하는 프로덕션 코드를 작성
- REFACTOR: 테스트가 통과하면 프로덕션 코드를 리팩토링
반면 단위 테스트는 TDD의 첫 번째 단계인 기능 단위 테스트 코드를 작성하는 것을 말한다.
TDD와 달리 테스트 코드를 꼭 먼저 작성해야 하는 것이 아니며 리팩토링이 포함되지 않는 순수 테스트 코드만을 작성하는 것을 의미한다.
테스트 코드를 작성하면 다음과 같은 이점이 있다.
- 단위 테스트는 개발단계 초기에 문제를 발견하게 도와준다.
- 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업데이트 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있다.
- 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있다.
- 단위 테스트는 시스템에 대한 실제 문서를 제공한다. 즉, 단위 테스트 자체가 문서로 사용될 수 있다.
테스트 코드 작성을 도와주는 프레임워크들은 다음과 같다.
- JUnit - Java
- DBUnit - DB
- CppUnit - C++
- NUnit - .net
우리는 자바용인 JUnit 버전 4를 사용하도록 한다.
프로젝트 환경 점검
- Java 8
- Gradle 4.x
- Spring Boot 2.1.x
Gradle 버전 낮추기
-인텔리제이에서 alt+F12 터미널 오픈 후 다음 명령어 입력
먼저 그레이들 프로젝트를 스프링 부트로 변경하기 위해 build.gradle 파일을 다음과 같이 수정한다.
buildscript{
ext{
springBootVersion = '2.1.7.RELEASE'
}
repositories {
mavenCentral()
jcenter()
}
/*프로젝트의 플러그인 의존성 관리를 위한 설정*/
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
plugins {
id 'java'
}
/*플러그인 의존성들을 적용할 것인지 결정하는 코드*/
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group 'org.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
/*각종 의존성들을 어떤 원격 장소에서 받을지 결정*/
repositories {
mavenCentral()
}
/*프로젝트 개발에 필요한 의존성 설정*/
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
test {
useJUnitPlatform()
}
Hello Controller 테스트 코드 작성하기
1. Application 클래스 생성
package com.jeonni.webservice;
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 읽기와 생성을 모두 자동으로 설정
- @SpringBootApplication이 있는 위치부터 설정을 읽어가기 때문에 해당 클래스는 항상 프로젝트 최상단에 위치해야 한다.
2. 컨트롤러 클래스 생성
package com.jeonni.webservice.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";
}
}
- @RestController
- 컨트롤러를 JSON을 변환하는 컨트롤러로 만들어 준다.
- 예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있도록 해준다고 생각하면 된다.
- @GetMapping
- HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 준다.
- 예전에는 @RequsetMapping으로 사용되었다.
- 이제 이 프로젝트에는 /hello로 요청이 오면 문자열 hello를 반환하는 기능을 가지게 되었다.
3. 테스트 코드 작성
package com.jeonni.webservice;
import com.jeonni.webservice.web.HelloController;
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)
@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)에 집중할 수 있는 어노테이션이다.
- 선언할 경우 @Controller, @ControllerAdvice 등을 사용할 수 있다.
- 단, @Service, @Component, @Repository 등은 사용할 수 없다.
- 여기서는 컨트롤러만 사용하기 때문에 선언한다.
- @Autowired: 스프링이 관리하는 빈(Bean)을 주입받는다.
- private MockMvc mvc: 웹 API를 테스트할 때 사용한다.
- 스프링 MVC 테스트의 시작점이다.
- 해당 클래스를 통해 HTTP GET, POST 등에 대한 API를 테스트할 수 있다.
- mvc.perporm(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의 결과를 검증한다.
- 응답 본문의 내용을 검증한다.
- Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증한다.
롬복 소개 및 설치하기
롬복은 자바 개발 시 자주 사용하는 코드 Getter, Setter, 기본생성자, toString 등을 어노테이션으로 자동 생성해준다.
프로젝트에 롬복을 추가하기 위해 build.gradle에 다음의 코드를 추가해보자.
...
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web')
/*롬복 라이브러리 등록*/
implementation('org.projectlombok:lombok')
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
...
롬복 설정은 프로젝트마다 설정해야 한다. 플러그인 설치는 한 번만 하면 되지만 build.gradle에 라이브러리를 추가하는 것과 Enable annotation processing를 체크하는 것은 프로젝트마다 진행해야 한다.
Hello Controller 코드를 롬복으로 전환
1. Dto 생성하기
package com.jeonni.webservice.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이 없는 필드는 생성자에 포함하지 않음
2. Dto에 적용된 롬복이 잘 동작하는지 확인하는 테스트 코드 작성
package com.jeonni.webservice.web.dto;
import org.junit.Test;
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
assertThat(dto.getName()).isEqualTo(name);
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
① assertThat
- assertj 라는 테스트 검증 라이브러리의 검증 메소드
- 검증하고 싶은 대상을 메소드 인자로 받음
- 메소드 체이닝이 지원되어 isEqualTo와 같이 메소드를 이어서 사용할 수 있음
② isEqualTo
- assertj 의 동등 비교 메소드
- assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공
※ Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용한 이유
(1) CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다.
- Junit의 assertThat을 쓰게 되면 is()와 같이 CoreMatchers 라이브러리가 필요하다.
(2) 자동완성이 더 확실하게 지원된다.
- IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 기능 지원이 약하다.
롬복의 @Getter, @RequiredArgsConstructor 로 생성자가 자동으로 생성되었다는 것을 증명하였다.
이제 HelloController에도 새로 만든 ResponseDto를 사용하도록 코드를 추가해보자.
package com.jeonni.webservice.web;
import com.jeonni.webservice.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 helloResponseDto(@RequestParam("name") String name, @RequestParam("amount") int amount){
return new HelloResponseDto(name, amount);
}
}
- @RequestParam
- 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
- 여기서는 외부에서 name(@RequsetParam("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만 허용
- 따라서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야 함 ex) String.valueOf(amount)
- jaonPath
- JSON 응답값을 필드별로 검증할 수 있느 메소드
- $를 기준으로 필드명을 명시
- 여기서는 name과 amount를 검증하므로 $.name, $.amount로 검증한다.
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱