견고한 서비스를 만들고자 하면 TDD 또는 최소한의 테스트 코드는 작성해야한다
학습에 앞서 한가지 짚고 넘어갈 점이 있는데 바로 TDD와 단위테스트는 다른이야기이다.
TDD는 테스트가 주도하는 개발을 의미
즉, 테스트 코드를 먼저 작성하는것부터 시작한다.
- 항상 실패하는 테스트를 먼저 작성
- 테스트가 통과하는 프로덕션 코드를 작성
- 테스트가 통과하면 프로덕션 코드를 리팩토링한다.
단위테스트는 TDD의 첫번째 단계인 기능단위의 테스트 코드를 작성하는 것을 의미한다.
이번 장에서는 단위 테스트 코드만 배울터이니 TDD를 배워보고 싶다면 아래 사이트를 참고해보자
https://repo.yona.io/doortts/blog/issue/1
단위 테스트를 진행하는 것은 많은 이유가 있고 이는 정보처리기사를 통해 배웠으므로
현재 스프링부트 관점에서 사용하는 이유를 말해보자면
- 빠른 피드백 -> 서버를 재실행 하지 않아도 된다,
- 로그나 sysout을 통해 확인한 것을 자동검증을 통해 편하게 테스팅
- 개발자가 만든 기능을 안전하게 보호 (하나의 기능 추가시 기존 기능이 문제가 생길 수 있음)
더군다나 이는 규모가 클수록 전부다 검사를 진행하기가 힘들다.
- 패키지 생성 : 패키지명은 웹 사이트 주소의 역순으로 해주는 것이 일반적이다.
- Application.class 작성
- HelloController 작성
- HelloControllerTest.class 작성
Application.class
package com.jojoldu.book.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 스프링부트의 자동설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정한다.
@SpringBootApplication // @SpringBootApplication 있는 클래스가 가장 최상단 디렉토리에 위치해야 한다.
public class Application {
public static void main(String[] args) { SpringApplication.run(Application.class,args); }
}
코드 리뷰
@SpringBootApplication어노테이션을 이용하여
1. **스프링 부트 자동설정**
2. **스프링 Bean 읽기와 생성을 모두 자동으로 설정**
특히나 @SpringBootApplication이 위치한 클래스로부터 설정을 읽어나가기 때문에
@SpringBootApplication이 위치한 클래스를 다른 클래스보다 프로젝트의 최상단에 위치시켜야한다.
___________________________________________________________________________________________________
SpringApplication.run 을 이용하여 내장 WAS(spring)를 실행시킨다.
이렇게 사용할 경우 Tomcat을 사용할 필요가 없으니 어떤 환경에서든지 단순 jar로만 배포가 가능하다
HelloController
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으로 반환하는 컨트롤러로 만들어준다.(즉, 값을 반환했다 보면 된다.)
* 예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준거이라 생각하면 된다.
참고로 기존 @Controller 는 String이나 Model로 반환시에 해당 이름의 파일을 호출했다.
___________________________________________________________________________________________________
@GetMapping
* HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어준다.
* 예전에는 @RequestMapping(method = RequsetMethod.GET)으로 사용했었다.
HelloControllerTest.class
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.config.auth.SecurityConfig;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.security.test.context.support.WithMockUser;
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;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.hamcrest.Matchers.is;
@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(controllers = HelloController.class)
* 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
* @Controller , @ControllerAdvice 등을 사용할 수 있다.
* 단, @Service , @Component , @Repository등은 사용할 수 없다.
* 즉, 컨트롤러에 관해서 테스팅 해주는 것이고 여기서는 Controller 테스트하기에 선언해준다.
다른 어노테이션 사용을 추가로 원하면 다른 어노테이션을 더 설정해주면 된다.
* 후에 OAuth2 연동시 SecurityConfig를 읽지만 CustomOAuth2UserService 를 읽지 못하는 경우가 생기는데 그때 필터로 처리해준다.
___________________________________________________________________________________________________
@Autowired
* 스프링이 관리하는 빈(bean-객체)을 주입받는다.
___________________________________________________________________________________________________
private MockMvc mvc;
* 웹 API를 테스트 할 때 사용한다.
* 스프링 MVC 테스트의 시작점이다.
* 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.
___________________________________________________________________________________________________
mvc.perform(get("/hello"))
* MockMvc를 통해 /hello 주소로 HTTP GET 요청을 한다. -> 실제 접속한 것처럼
* 체이닝이 지원되어 여러 검증 기능을 이어서 선언할 수 있따.
___________________________________________________________________________________________________
(체이닝)
.andExpect(staus().isOk())
* mvc.perform의 결과를 검증한다.
* HTTP Header의 Status를 검증한다.(반환코드)
* 200 이면 성공이고 이외의 값이면 문제가 있다는 것이다.
.andExpect(content().string(hello))
* mvc.perform의 결과를 검증한다.
* 응답 본문의 내용을 검증한다. (json이면 json 내용 페이지면 페이지 전체 코드)
* Controller에서 "hello"를 리턴하기 때문에 맞다.
실제 우리가 코드를 작성함에 있어 수동으로 검증하고 테스트 코드를 작성하진 않는다.
테스트 코드로 먼저 검증 후, 정말 못 믿겠다는 생각이 들 땐 프로젝트를 실행해 확인한다.
롬복은 자바 개발자들이 자주 사용하는 Getter
, Setter
, 기본생성자
, toString
등을 자동 생성해준다.
인텔리제이에선 플러그인과 gradle 덕분에 쉽게 설정이 가능하다.
- build.gradle에 롬복 설정하기
build.gradle
dependecies{
...
compile('org.projectlombok:lombok')
...
}
- 롬복 플러그인 설치하기
- Command + shift + A 로 Action 진입 (MAC 버전)
- plugins 입력후 클릭
- 플러그인 설치 팝업의 Marketplace 탭으로 이동하여 lombok 검색
- lombok 설치 진행 및 재시작
- 롬복에 대한 설정이 필요하다는 팝업창 등장
- 클릭하여 설정해야 해야할 장소인 파란 링크를 클릭
- Enable annotaion processing 의 체크박스를 체크한다.
HelloResponseDto
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이 없는 필드는 생성자에 포함되지 않는다.
생성자를 생성하고 생성자를 통해서 값을 주입 받는다 생각하면 된다.
HelloResponseDtoTest
package com.jojoldu.book.springboot.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()의 값을 비교해서 같을때만 성공이다.
위 코드에서는 값이 변경없이 잘 들어갔나 확인하는 것이라 보면된다.
asserThat은 사실 JUnit에도 존재하지만 assertj 라이브러리의 asserThat을 이용했다.
JUnit 과 assertj 를 비교해보자면 이렇다.
- CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다.
- JUnit의 asserThat을 사용시
is()
사용을 원하면 CoreMatchers 라이브러리를 추가로 필요하다.
- JUnit의 asserThat을 사용시
- 자동 완성이 좀 더 확실하게 지원됩니다.
- IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약하기 때문이다.
보다 자세한 설명은 백기선님의 유튜브 https://www.youtube.com/watch?v=zLx_fI24UXM 를 참고하자
- IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약하기 때문이다.
HelloController 수정
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로 넘긴 파라미터를 가져오는 어노테이션이다.
* 여기서는 외부에서 "name" 이란 이름으로 넘긴 파라미터를 name 변수에 저장한다.
HelloControllerTest 수정
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.config.auth.SecurityConfig;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.security.test.context.support.WithMockUser;
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;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.hamcrest.Matchers.is;
@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));
}
@Test
public void helloDto가_리턴된다() throws Exception{
System.out.println(123);
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("$.name", is(name))
* JSON 응답값을 필드별로 검증할 수 있는 메소드이다.
* $를 기준으로 필드명을 명시한다.
* 여기서는 name 과 amount를 검증하니 $.name, $.amount로 검증한다.