2020. 8. 12. 16:29ㆍSpring Boot
일반적으로 단위테스트 코드를 작성할 때 5가지 원칙을 강조한다. (FIRST원칙)
F - Fast (테스트 코드는 빠르게 실행되어야 한다)
I - Independent (독립적으로 실행되어야 한다)
R - Repeatable (반복 실행 가능해야 한다)
S - Self Validating (메뉴얼 없이 테스트 코드만 성공해도 성공,실패여부를 파악할 수 있어야한다)
T - Timely (즉시 사용 가능해야 한다)
스프링 부트 테스트 디펜던시
스프링 부트는 애플리케이션 테스트를 위한 많은 기능을 제공한다. 크게 두 가지 모듈을 지원한다.
- spring-boot-test : 핵심 기능 포함
- spring-boot-test-configuration : 테스트를 위한 AutoConfiguration 제공
앞 게시글(그레이들(Gradle)이란 무엇인가)에서 build.gradle 에 작성한 내용 중
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
...
}
에서 testCompile메소드의 ‘org.springframework.boot:spring-boot-starter-test’ 의존성을 주입하면 필요한 디펜던시를 자동으로 설정해준다.
- JUnit 4 : The de-facto standard for unit testing Java applications.
- Spring Test & Spring Boot Test: Utilities and integration test support for Spring Boot applications.
- AssertJ : A fluent assertion library.
- Hamcrest : A library of matcher objects (also known as constraints or predicates).
- Mockito : A Java mocking framework.
- JSONassert : An assertion library for JSON.
- JsonPath : XPath for JSON.
p.62 HelloControllerTest 분석하기
분석에 앞서 전체 코드를 보자.
@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 어노테이션
@Runwith 은 jUnit 프레임워크 테스트 실행방법을 확장할 때 사용하는 어노테이션이다. 책에서는 @Runwith(SpringRunner.class) 처럼 SpringRunner.class 클래스를 인자로 받는데, 이렇게 하면 jUnit에 내장된 러너를 사용하는 대신 SpringRunner라는 스프링 실행 러너를 이용한다.
즉 @Runwith(SpringRunner.class) 는 스프링 부트와 jUnit 사이의 연결자 역할을 한다.
@WebMvcTest 어노테이션
스프링 부트의 컨트롤러를 jUnit으로 테스트 하고 싶을경우 41.3.7 Auto-configured Spring MVC tests를 보면 Http Connection을 별도로 구현하지 않아도 MVC 테스트를 가능하게 하는 설명이 나온다.
To test whether Spring MVC controllers are working as expected, use the @WebMvcTest annotation. @WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer, and HandlerMethodArgumentResolver. Regular @Component beans are not scanned when using this annotation.
의역 : @WebMvcTest 자체의 기능은 컨트롤러가 예상대로 동작하는지 테스트하는 것이다. 다만, @Controller, @ControllerAdvise, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver 이 붙은 클래스만 스캔하도록 제한한다.
또한 @WebMvcTest를 사용하기 위해 테스트할 특정 컨트롤러 클래스를 명시하도록 한다.
따라서, 예제의 @WebMvcTest(controllers = HelloController.class) 는 HelloController 클래스만 테스트 하도록 명시한다는 의미이다.
MockMvc
mockmvc란 웹 애플리케이션을 애플리케이션 서버에 배포하지 않고도 스프링 mvc의 동작을 재현할 수 있는 클래스이다.
흐름
테스트 케이스의 메서드는 DispathcherServlet에 요청할 데이터(요청경로나 요청파라미터)를 설정
MockMvc는 DispathcherServlet에 요청을 보냄
- DispathcherServlet은 테스트용으로 확장된 TestDispathcherServlet
DispathcherServlet은 요청을 받아 매핑정보를 보고 그에 맞는 핸들러(컨트롤러) 메서드 호출
테스트 케이스 메서드는 MovkMvc가 반환하는 실행결과를 받아 실행 결과가 맞는지 검증한다.
MockMvc - perform() 메소드
MockMvc Docs를 보면 MockMvc는 preform 이라는 메소드를 지원하고 있는 것을 볼 수 있다.
perform() 메소드는
- DispatcherServlet에 요청을 의뢰하는 역할을 한다.
- MockMvcRequestBuilder클래스를 사용해 설정한 요청 데이터를 perform()의 인수로 전달한다.
- get, post, put, delete, fileUpload 와 같은 메서드를 제공한다.
- ResultActions 이라는 인터페이스를 반환한다.
요청 데이터 설정
perform 메소드로 테스트 할 url을 설정했다면 이 url이 요청할 데이터 또한 설정할 수 있다.
MockHttpServletRequestBuilder 클래스가 이 기능을 제공한다.
MockHttpServletRequestBuilder의 주요메소드는 다음과 같다.
-
param / params
요청 파라미터 설정 -
header / headers
요청 해더 설정
contentType & accept와 같은 특정 해더를 설정하는 메서드도 제공 -
cookie
쿠키 설정 -
content
요청 본문 설정 -
requestAttr
요청 스코프에 객체를 설정 -
flashAttr
플래시 스코프에 객체를 설정 -
sessionAttr
세션 스코프에 객체를 설정
예시 코드처럼 사용이 가능하다.
@Test
public void testBooks() throws Exception {
mockMvc.perform(get("books"))
.param("name", "Spring")
.accept(MediaType.APPLICATION_JSON)
.header("X-Track-Id", UUID.randomUUID().toString())
.andExpect(status().isOk());
}
ResultActions 인터페이스
ResultAction 인터페이스 Docs를 보면
- andDo()
- andExpect()
- andReturn()
세 가지 메소드를 지원하고 있는 것을 확인할 수 있다.
1. andDo 메소드
andDo() 메소드를 도큐먼트에서 찾아보면 “Perform a general action.” 이라는 아주 간단한 설명만 나온다.
이 메소드는 인수에 실행 결과를 처리할 수 있는 ResultHandler를 지정해서 사용하는데, 이 ResultHandler는 log 메소드와 print 메소드를 지원하고 있다.
-
log() 실행결과를 디버깅 레벨에서 로그로 출력
-
print() 실행결과를 임의의 출력대상에 출력 출력대상을 지정하지 않으면 System.out 으로 출력한다.
mvc.perform(get("/hello"))
.andDo(print())
...
예를 들어, 위 코드처럼 andDo(print())를 사용하게 되면 MockHttpServletRequest, MockHttpServletResponse, Handler 등의 실행결과를 출력한다.
- andDo(print()) 결과로그
MockHttpServletRequest:
HTTP Method = GET
Request URI = /hello
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@ca25360: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER}
Handler:
Type = com.jojoldu.book.springboot.web.HelloController
Method = public java.lang.String com.jojoldu.book.springboot.web.HelloController.hello()
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"5", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = text/plain;charset=UTF-8
Body = hello
Forwarded URL = null
Redirected URL = null
Cookies = []
2. andExpect 메소드
인수에 실행결과를 검증하는 MockMvcResultMatchers에서 제공하는 ResultMatcher 을 지정한다.
MockMvcResultMatchers 에서 제공하는 주요메소드는 다음과 같다.
-
status
HTTP 상태 코드 검증 -
header
응답 해더의 상태 검증 -
cookie
쿠키 상태 검증 -
content
응답 본문 내용 검증 jsonPath나 xpath와 같은 특정 콘텐츠를 위한 메서드도 제공 -
view
컨트롤러가 반환한 뷰 이름 검증 -
forwardedUrl
이동대상의 경로를 검증 패턴으로 검증할떄는 forwardedUrlPattern 메서드를 사용 -
redirectedUrl
리다이렉트 대상의 경로 또는 URL 검증 패턴으로 검증할때는 redirectedUrlPattern 메서드를 사용 -
model
스프링 MVC 모델 상태 검증 -
flash
플래시 스코프의 상태 검증 -
request
서블릿 3.0부터 지원되는 비동기 처리의 상태나 요청 스코프의 상태, 세션 스코프의 상태 검증
따라서, 아래 코드의 andExpcet 메소드 인수로 사용되는 status와 content 메소드로 perform() 의 결과를 검증한다.
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
3. andReturn 메소드
return 결과를 반환할 때 쓰인다. 당연히 void 메소드를 테스트할 때 사용할 수 없다.
p.62 HelloControllerTest 정리
@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));
}
}
위에서 알아본 것들을 토대로 HelloControllerTest를 정리하면서 마무리해보자.
- 1행의 RunWith(SpringRunner.class) 로 Spring Boot 와 jUnit를 연결하여 스프링부트 환경으로 jUnit을 실행한다.
- 2행의 @WebMvcTest(controllers = HelloController.class) 으로 컨트롤러를 테스트한다. 이때 인수값으로 HelloController.class를 넣어줌으로써 테스트할 클래스를 명시한다.
- 6행의 Mockmvc클래스를 선언함으로써 Spring mvc의 동작을 재현할 메소드를 호출할 준비를 한다.
- 12행의 perform(get(“/hello”) 로 /hello url 을 GET 방식으로 요청한다.
- 13행의 andExpect(status().isOk()) 으로 요청 시 상태값이 200(요청성공)인지 확인한다.
- 14행의 andExpect(content().string(hello)) 으로 ‘컨트롤러의’ 응답 본문의 내용이 문자열 “hello”인지 검증한다.
p.73 HelloResponseDtoTest 분석하기
HelloResponseDtoTest 분석에 앞서 HelloResponseDto가 어떤 내용을 담고 있는지 확인하자.
@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
private final String name;
private final int amount;
}
이 클래스에는 롬복이 적용되어 있다. 롬복의 @Getter와 @RequestArgsConstructor 에 대한 설명은 여기에서 하지 않는다.
필드에 name 과 amount 가 final 로 선언되어 있다. 따라서 name과 amount 모두를 인자로 하는 생성자가 만들어져 있는 상태이다. (@Getter의 효과로 getName() 과 getAmount() 역시 생성됐다.)
이제 이 클래스에 롬복이 제대로 적용되어 있는지 테스트를 해보려고 한다.
테스트 코드는 다음과 같다.
public class HelloResponseDtoTest {
@Test
public void 롬복_기능_테스트(){
//given
String name = "test";
int amount = 10000;
//when
HelloResponseDto dto = new HelloResponseDto(name, amount);
//then
assertThat(dto.getName()).isEqualTo(name);
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
본격적인 뜯어보기에 앞서, 이 책의 거의 모든 테스트 코드에는 given / when / then 주석이 있다. 필자는 given(주어진), when(언제), then(그러면) .. 식의 직역을 했었는데 그러다보니 이것들이 정확히 무엇을 의미하는지 파악하지 못했다.
찾아보니 이것은 Given - When - Then 패턴이었다. Martin Fowler의 글에 따르면
Given-When-Then 은 대표적인 테스트 방식으로, Daniel Terhorst-North 와 Chris Matts 가 행위주도개발(Behavior-Driven-Development BDD)의 방식의 접근법이다.
Given - When - Then 은 곧 [준비 - 실행 - 검증] 이다.
Given
테스트를 위해 준비를 하는 과정이다. 테스트에 사용하는 변수, 입력 값 등을 정의하거나 Mock 객체를 정의하는 구문도 Given에 포함된다.
//given
String name = "test";
int amount = 10000;
name 에 “test”, amount 에 10000 값을 입력했다.
When
실제로 액션을 하는 테스트를 실행하는 과정이다.
//when
HelloResponseDto dto = new HelloResponseDto(name, amount);
HelloResponseDto(name, amount) 생성자로 dto 객체를 생성했다. 정상적으로 롬복이 적용됐다면 dto 객체의 필드 name 과 amount 에는 각각 “test” 와 10000 값이 들어가 있을 것이다.
Then
테스트를 검증하는 과정이다.
//then
assertThat(dto.getName()).isEqualTo(name);
assertThat(dto.getAmount()).isEqualTo(amount);
assertThat 메소드로 When 에서 얻어낸 dto의 필드값과 Given에서 입력한 값을 isEqualTo 메소드로 비교한다. 두 값이 같으면 테스트가 성공한다.
assertThat 메소드
스프링 4.4로 넘어오면서 기존에 쓰던 assertEquals 대신에 assertThat 메서드가 추가되었다. 기존의 assertEquals(test, actual) 방식이 가독성이 떨어지고 쉽게 헷갈릴 수 있기 때문에 개선된 형태가 assertThat(test, is(expect)) 방식이다.
하지만, 이 assertThat 은 jUnit 에서 기본적으로 제공하는 메소드로 책에서는 쓰지 않는다. 책에서는 AssertJ에서 제공하는 assertThat() 메소드를 사용한다.
AssertJ의 개념과 그 실제 사용법에 대해서는 다음 게시물에 자세히 정리하려고 한다.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] (3) 로그인/회원가입 - 자사 회원가입 :: 음악학원 홈페이지 프로젝트 (0) | 2020.08.14 |
---|---|
[Spring Boot] (2) 로그인/회원가입 - Spring Security 설정 :: 음악학원 홈페이지 프로젝트 (7) | 2020.08.12 |
[Spring Boot] (1) Gradle 빌드업 :: 음악학원 홈페이지 프로젝트 (0) | 2020.08.12 |
AssertJ에 대하여 (1) | 2020.08.12 |
그래이들(Gradle)이란 무엇인가? (1) | 2020.08.12 |