TDD는 테스트 주도 개발을 의미한다.
즉, test code를 먼저 작성하는 것으로 시작한다.
사진 출처 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 : 이동욱 (책)
단위테스트는 TDD의 첫번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 의미한다.
■ 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.
■ 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에
서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다(예, 회귀 테스트).
■ 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.
■ 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다.
일반적으로 패키지 명은 웹사이트 주소의 역순으로 한다.
package orm.example.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 class는 메인 클래스로 사용할 예정입니다.
스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정합니다.
여기에선 내장 WAS로 사용할 것입니다.
(내장 WAS : 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것)
이유는
언제 어니서나 같은 환경에서 스프링 부트를 배포할수 있기 때문입니다.
HelloController class
package orm.example.springboot.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";
}
}
Annotation설명
package orm.example.springboot;
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 org.springframework.test.web.servlet.ResultActions;
import orm.example.springboot.web.HelloController;
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_returned() throws Exception {
String hello = "hello";
mvc.perform(get("/hello")) // 5
.andExpect(status().isOk()) // 6
.andExpect(content().string(hello)); // 7
}
}
코드 및 Annotation설명
@RunWith(SpringRunner.class)
• 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킵니다.
• 여기서는 SpringRunner라는 스프링 실행자를 사용합니다.
• 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 합니다.
사용 이유 : RunWith 대신 SpringBoot를 사용하면 application context를 전부 로딩하여 자칫 무거운 프로젝트가 될 수 있음. Junit4에서 필요한 조건에 맞춰서 사용하기 위함
@WebMvcTest
• 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션 입니다.
• 선언할 경우 @Controller, @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 등의 상태를 검증합니다. (여기선 OK 즉, 200인지 아닌지를 검증합니다.)
andExpect(content( ).string(hello))
• mvc.perform의 결과를 검증합니다.
• 응답 본문의 내용을 검증합니다.
• Controller에서 “hello”를 리턴하기 때문에 이 값이 맞는지 검증합니다.
import org.junit.Test;
import org.junit.runner.RunWith;
@WebMvcTest(controllers = HelloController.class) 에 에러가 발생했습니다.
Testing과 Junit4를 경로에 추가해주고
import orm.example.springboot.web.HelloController;
를 추가해주어 문제를 해결하였습니다.
테스트가 통과된 것을 확인 할 수 있습니다.
기존에 작성한 application.java 파일에서 실행을 시켜보면 tomcat 서버가 8080으로 실행된 것을 확인 할 수 있습니다.
localhost:8080으로 그냥 요청하게 되면
이렇게 404 에러가 발생하며 요청한 api를 찾을 수 없다고 나옵니다. 저는 hello라는 url에 맞는 api만 생성하였기 떄문에 /hello까지 입력을 해주어야 합니다.
개발시 중요한 마음가짐
수동으로 검증후 테스트 코드를 작성하지 않는다.
테스트 코드 먼저 검증 후, 정만 못 믿겠을 때 프로젝트를 실행하여 확인해야 한다.
//src/main/java/com/jojoldu/book/springboot/web/dto/
package orm.example.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // 1
@RequiredArgsConstructor // 2
public class HelloResponseDto {
private final String name;
private final int amount;
}
package orm.example.springboot.dto;
import org.junit.Test;
import orm.example.springboot.web.dto.HelloResponseDto;
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
//test용 name과 amount를 초기화하고 name과 amount를 인자로 dto 객체를 생성해 현재 지역 변수인 name과 amount와 같은지 testing하는 코드
assertThat(dto.getName()).isEqualTo(name); // 1, 2
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
Junit의 기본 assertThat이 아닌 assertj의 assertThat을 사용한 이유
1. CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않습니다.
-> Junit의 assertThat을 쓰게 되면 is( )와 같이 CoreMatchers 라이브러리가 필요합 니다.
2. 자동완성이 좀 더 확실하게 지원됩니다.
-> IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약합니다.
에러 발생시 이 글을 참고하세요
테스트 결과
테스트 성공으로 HelloResponseDto에 getter와 생성자를 따로 선언해두지 않았음에도 @Getter로 get 메소드가, @RequiredArgsConstructor로 생성자가 자동으로 생성되는 것이 증명되었습니다.
package orm.example.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // 1
@RequiredArgsConstructor // 2
public class HelloResponseDto {
private final String name;
private final int amount;
}
package orm.example.springboot.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import orm.example.springboot.web.dto.HelloResponseDto;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/Hello/dto")
public HelloResponseDto helloDto(
@RequestParam("name") String name, //외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
@RequestParam("amount") int amount) {
return new HelloResponseDto(name, amount);
}
}
package orm.example.springboot;
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 orm.example.springboot.web.HelloController;
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.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@RunWith(SpringRunner.class) // 1
@WebMvcTest(controllers = HelloController.class) // 2
public class HelloControllerTest {
@Autowired // 3
private MockMvc mvc; // 4
@Test
public void hello_returned() throws Exception {
String hello = "hello";
mvc.perform(get("/hello")) // 5
.andExpect(status().isOk()) // 6
.andExpect(content().string(hello)); // 7
}
@Test
public void helloDto_returned() throws Exception {
String name = "hello";
int amount = 1000;
mvc.perform(get("hello/dto")
//8
.param("name", name).param("amount", String.valueOf(amount)))
//9
.andExpect(status().isOk()).andExpect(jsonPath("$.name", is(name))).andExpect(jsonPath("$.amount", is(amount)));
}
}
param
• API 테스트할 때 사용될 요청 파라미터를 설정합니다.
• 단, 값은 String만 허용됩니다. -> amount를 string으로 바꾼 이유
• 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다.
jsonPath
• JSON 응답값을 필드별로 검증할 수 있는 메소드입니다.
• $를 기준으로 필드명을 명시합니다.
• 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증합니다.
처음엔 실패했는데 이유는 HelloController에 api경로를 /hello/dto가 아닌 /Hello/dto로 오타가 있었다...