스프링 부트 테스트에 대하여

2020. 8. 12. 16:29Spring 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.
 

Testing

The classes in this example show the use of named hierarchy levels in order to merge the configuration for specific levels in a context hierarchy. BaseTests defines two levels in the hierarchy, parent and child. ExtendedTests extends BaseTests and instruct

docs.spring.io

 

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에 요청을 보냄

 

 

TestDispatcherServlet

Render the given ModelAndView. This is the last stage in handling a request. It may involve resolving the view by name.

docs.spring.io

DispathcherServlet은 요청을 받아 매핑정보를 보고 그에 맞는 핸들러(컨트롤러) 메서드 호출

테스트 케이스 메서드는 MovkMvc가 반환하는 실행결과를 받아 실행 결과가 맞는지 검증한다.

 

MockMvc - perform() 메소드

MockMvc Docs를 보면 MockMvc는 preform 이라는 메소드를 지원하고 있는 것을 볼 수 있다.

 

MockMvc (Spring Framework 5.2.8.RELEASE API)

 

docs.spring.io

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를 보면

 

ResultActions (Spring Framework 5.2.8.RELEASE API)

Perform an expectation. Example static imports: MockMvcRequestBuilders.*, MockMvcResultMatchers.* mockMvc.perform(get("/person/1")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.person.name

docs.spring.io

  • 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 에서 제공하는 주요메소드는 다음과 같다.

 

MockMvcResultMatchers (Spring Framework 5.2.8.RELEASE API)

Static factory methods for ResultMatcher-based result actions. Eclipse Users Consider adding this class as a Java editor favorite. To navigate to this setting, open the Preferences and type "favorites".

docs.spring.io

  • 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)의 방식의 접근법이다.

 

bliki: GivenWhenThen

 

bliki: GivenWhenThen

a bliki entry for GivenWhenThen

martinfowler.com

 

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의 개념과 그 실제 사용법에 대해서는 다음 게시물에 자세히 정리하려고 한다.