본문 바로가기
공부 기록

[Rest Assured] Rest Assured로 Mock과 함께 Controller Unit Test 하기

by 타태 2022. 7. 9.

 

2022.06.11 - [Back-End/Java] - [JAVA] Socket with JDK

 

[JAVA] Socket with JDK

2022.04.16 - [실전 공부] - [JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로드 구현하기 [JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로

ktae23.tistory.com

 

Rest Assured

Rest Assured의 기본 문법은 BDD (Behavior Driven Development)와 매우 유사하다.

 

Given() 
        .param("x", "y") 
        .header("z", "w")
.when()
.Method()
.Then() 
        .statusCode(XXX)
        .body("x, ”y", equalTo("z"));

 


Code Explanation
Given() 요청의 기반 설정을 할 수 있습니다. 요청 헤더, 쿼리 스트링 또는 경로 변수, 리퀘스트 바디 (컨텐트), 쿠키 등을 전달합니다. 이러한 항목이 필요하지 않을 경우 사용하지 않는 선택 사항입니다.
When() 요청을 처리하는 시나리오 전체를 나타냅니다.
Method() CRUD 작업을 수행하는 HTTP 메서드를 Method 자리에 사용합니다.(get/post/put/delete)
Then() 단언문이나 일치 조건을 선언합니다.

학습 테스트를 통해 Rest Assured 사용법을 코드로 확인해보자.


maven dependency

java 9 미만 버전
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>xml-path</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>

java 9이상 버전
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured-all</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>


학습 테스트

테스트 코드를 먼저 작성해보자.

Rest Assured는 주로 시나리오 기반 통합 테스트 목적으로 사용되기 때문에 테스트 메서드를 모아 둔 클래스를 따로 작성하고 이를 호출하여 결과를 검증하는 테스트를 한 곳에서 수행하는 편이다.

TestMain

    private static final long MEMBER_ID = 1L;
    private static final String SUCCESS_MESSAGE = "test String " + MEMBER_ID;

    @Test
    @DisplayName("get method test")
    void getMethodTest() {
        final ExtractableResponse<Response> testString = getTestString(MEMBER_ID);
        Assertions.assertEquals(SUCCESS_MESSAGE , testString.response().body().asString());
        Assertions.assertEquals(HttpStatus.OK.value(), testString.response().statusCode());
    }

TestControllerClass

class TestControllerClass {
    static ExtractableResponse<Response> getTestString(Long memberId) {
        return given()
                    .log().all()
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .header("Authorization", memberId)
                    .pathParam("memberId", memberId)
                .when()
                    .get("/test/{memberId}")
                .then()
                    .log().all()
                .extract();
    }
}

테스트 코드를 작성한 뒤 이를 받아줄 컨트롤러를 작성한다.

 

TestController

@RequestMapping("/test")  
@RestController  
public class TestController {  

  @GetMapping("/{memberId}")  
  public ResponseEntity<String> getTestString(@PathVariable Long memberId) {  
      return ResponseEntity.ok("test String " + memberId);  
  }  
}

그리고 바로 실행해보겠습니다. 결과는 아래와 같다.

java.net.ConnectException: Connection refused: connect

찾아보니 RestAssured의 포트를 랜덤 포트로 설정하여 넣어주는 설정이 필요하다고 한다.
아래와 같이 추가해보자.


TestMain

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestMain {

    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }
    //...
}


변경 뒤 실행하니 초록불이 들어오며 첫 테스트에 성공한다.

Request method:    GET
Request URI:    http://localhost:65358/test/1
Proxy:            <none>
Request params:    <none>
Query params:    <none>
Form params:    <none>
Path params:    memberId=1
Headers:        Authorization=1
                Accept=*/*
                Content-Type=application/json; charset=UTF-8
Cookies:        <none>
Multiparts:        <none>
Body:            <none>
2022-07-09 17:16:44.745  INFO 18468 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-07-09 17:16:44.745  INFO 18468 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-07-09 17:16:44.746  INFO 18468 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Sat, 09 Jul 2022 08:16:44 GMT
Keep-Alive: timeout=60
Connection: keep-alive

test String 1

Rest AssuredMockMvc와 달리 대부분 End to End 테스트에 사용하며 @SpringBootTest로 실제 요청을 보내서 전체적일 로직을 테스트하는데 사용한다.

때문에 순수한Contoller 레이어만 분리하여 테스트하는 MockMvc와 달리 서비스를 비롯한 연관 된 스프링 빈들에 대한 테스트가 함께 수행 된다.

만일 Rest Assured를 이용하면서 MockMvc로 테스트 하듯이 레이어 테스트를 원한다면 별도의 설정을 통해 Controller Layer만 분리하여 단위 테스트를 할 수 있다.

 


Rest Assured로 MockMvc 테스트 하기

먼저 MockMvc 사용을 위한 의존성을 추가.

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>spring-mock-mvc</artifactId>
    <version>4.2.0</version>
    <scope>test</scope>
</dependency>

`RestAssuredMockMvc`를 사용하여 테스트를 할때는 어떤 방식으로 실행 할 것인지 선택해야 한다. `Stand Alone`과 `Web Application Context` 중 선택할 수 있고, 실행 방식을 테스트 당 한번으로 사용할 수 있다.

Standalone 방식

한 개 이상의 @Controller 또는 @ControllerAdvice 어노테이션이 있는 클래스를 이용해 RestAssuredMockMvc를 초기화 한다.

import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;

    public static ExtractableResponse<Response> getTestString(Long memberId) {
        return given().standaloneSetup(new TestController()) 
        // 또는 TestMain에서 @Autowired 후 파라미터로 전달 받아 사용

또는 여러 테스트를 수행하는 경우에는 미리 설정을 해둘 수 있다.

    @BeforeEach
    void portSetUp() {
        RestAssured.port = port;
        RestAssuredMockMvc.standaloneSetup(new TestController());
    }

standaloneSetup()의 시그니처는 아래와 같다. 가변 인자를 사용하기 때문에 하나 이상의 대상을 사용할 수 있다.

    public static void standaloneSetup(Object... controllersOrMockMvcConfigurers) {
        mockMvcFactory = StandaloneMockMvcFactory.of(controllersOrMockMvcConfigurers);
    }

Web Application Context 방식

spring 의 WebApplicationContext을 이용하는 방식. standaloneSetup()과 유사한 방식으로 테스트 마다 초기화 할 수 있다.

TestMain에서 WebApplicationContext를 주입 받아 인자로 넘겨 준다.

// TestMain

    @Autowired
    private WebApplicationContext webApplicationContext;
// ===================================
// TestContreollerClass

    public static ExtractableResponse<MockMvcResponse> getTestString(Long memberId, WebApplicationContext context) {
        return given().webAppContextSetup(context)
                    .log().all()
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .header("Authorization", memberId)
                .when()
                    .get("/test/{memberId}", memberId)
                .then()
                    .log().all()
                .extract();
    }

또는 여러 테스트를 위해 미리 설정해둘 수 있다.

    @BeforeEach
    void portSetUp() {
        RestAssured.port = port;
        RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
     }

Rest Assured를 사용해 Controller Unit Test를 하기 위한 Mock 설정 방법을 배웠다.
이제 아래와 같이 standaloneSetup()과 mock을 주입한다.

tandaloneSetup 방식은 사용자가 제공한 mock만을 이용해서 초기화하기 때문에 유닛 테스트 목적으로 적합하다.

그리고 이외에 테스트에 필요한 여러 클래스를 추가한다.

 


BadRequestException (for test)

package com.example.restassuredstudy;  

public class BadRequestException extends RuntimeException {  
  public BadRequestException() {  
      super();  
  }  
}

TestControllerExceptionHandler

package com.example.restassuredstudy;  

import org.springframework.http.HttpStatus;  
import org.springframework.web.bind.annotation.ExceptionHandler;  
import org.springframework.web.bind.annotation.ResponseStatus;  
import org.springframework.web.bind.annotation.RestControllerAdvice;  
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;  

@RestControllerAdvice(assignableTypes = TestController.class)  
public class TestControllerExceptionHandler extends ResponseEntityExceptionHandler {  

  @ResponseStatus(HttpStatus.BAD_REQUEST)  
  @ExceptionHandler(BadRequestException.class)  
  private void badRequest(BadRequestException badRequestException){  
 }}

TestService

package com.example.restassuredstudy;  

import org.springframework.stereotype.Service;  

@Service  
public class TestService {  

    public String getTestString(Long memberId) {  
    if (1L == memberId) {  
        return "test String " + memberId;  
    }  
    throw new BadRequestException();  
    }  
}

그리고 실패 테스트 케이스와 다른 방식의 테스트 코드를 추가한다.

처음에 Rest Assured가 시나리오 기반의 통합/인수 테스트를 위해 사용되는 방식에 대해 설명했는데
이제는 컨트롤러 유닛 테스트를 진행하기 때문에 테스트 검증을 바로 수행하는 케이스를 추가해본다.


TestMain

package com.example.restassuredstudy;  

import io.restassured.module.mockmvc.RestAssuredMockMvc;  
import io.restassured.module.mockmvc.response.MockMvcResponse;  
import io.restassured.response.ExtractableResponse;  
import joptsimple.internal.Strings;  
import org.hamcrest.Matchers;  
import org.junit.jupiter.api.Assertions;  
import org.junit.jupiter.api.BeforeEach;  
import org.junit.jupiter.api.DisplayName;  
import org.junit.jupiter.api.Test;  
import org.junit.jupiter.api.extension.ExtendWith;  
import org.mockito.InjectMocks;  
import org.mockito.Mock;  
import org.mockito.Mockito;  
import org.mockito.junit.jupiter.MockitoExtension;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.MediaType;  

import static com.example.restassuredstudy.TestControllerClass.getTestString;  
import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;  
import static org.hamcrest.Matchers.equalTo;  
import static org.hamcrest.core.Is.is;  

@ExtendWith(MockitoExtension.class)  
class TestMain {  

    @Mock  
    private TestService testService;  
    @InjectMocks  
    private TestController testController;  
    @InjectMocks  
    private TestControllerExceptionHandler testControllerExceptionHandler;  

    private static final long MEMBER_ID = 1L;  
    private static final long FAIL_MEMBER_ID = 2L;  
    private static final String SUCCESS_MESSAGE = "test String " + MEMBER_ID;  

    @BeforeEach  
    void setUp() {  
        RestAssuredMockMvc.standaloneSetup(testController, testControllerExceptionHandler);  
    }  

    @Test  
    @DisplayName("get method test")  
    void getMethodTest() {  
        Mockito.when(testService.getTestString(MEMBER_ID)).thenReturn(SUCCESS_MESSAGE);  

        final ExtractableResponse<MockMvcResponse> testString = getTestString(MEMBER_ID);  

        Assertions.assertEquals(SUCCESS_MESSAGE, testString.response().body().asString());  
        Assertions.assertEquals(HttpStatus.OK.value(), testString.response().statusCode());  
    }  

    @Test  
    @DisplayName("get method test another version")  
    void getMethodTestAnotherVersion() {  
        Mockito.when(testService.getTestString(MEMBER_ID)).thenReturn(SUCCESS_MESSAGE);  

        given()  
            .log().all()  
            .contentType(MediaType.APPLICATION_JSON_VALUE)  
            .header("Authorization", MEMBER_ID)  
        .when()  
            .get("/test/{memberId}", MEMBER_ID)  
        .then()  
            .log().all()  
        .assertThat()  
            .statusCode(HttpStatus.OK.value())  
            .body(is(equalTo(SUCCESS_MESSAGE)));  
    }  

    @Test  
    @DisplayName("get method fail test")  
    void getMethodFailTest() {  
        Mockito.when(testService.getTestString(FAIL_MEMBER_ID)).thenThrow(new BadRequestException());  

        final ExtractableResponse<MockMvcResponse> testString = getTestString(FAIL_MEMBER_ID);  

        Assertions.assertTrue(Strings.EMPTY.equals(testString.response().body().asString()));  
        Assertions.assertEquals(HttpStatus.BAD_REQUEST.value(), testString.response().statusCode());  
    }  

    @Test  
    @DisplayName("get method fail test another version")  
    void getMethodFailTestAnotherVersion() {  
        Mockito.when(testService.getTestString(FAIL_MEMBER_ID)).thenThrow(new BadRequestException());  

        given()  
            .log().all()  
            .contentType(MediaType.APPLICATION_JSON_VALUE)  
            .header("Authorization", FAIL_MEMBER_ID)  
        .when()  
            .get("/test/{memberId}", FAIL_MEMBER_ID)  
        .then()  
            .log().ifValidationFails()  
        .assertThat()  
            .statusCode(HttpStatus.BAD_REQUEST.value())  
            .body(Matchers.emptyString());  
    }  
}

TestControllerClass

package com.example.restassuredstudy;  

import io.restassured.module.mockmvc.response.MockMvcResponse;  
import io.restassured.response.ExtractableResponse;  
import org.springframework.http.MediaType;  

import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;  


class TestControllerClass {  

  public static ExtractableResponse<MockMvcResponse> getTestString(Long memberId) {  
  return  given()  
            .log().all()  
            .contentType(MediaType.APPLICATION_JSON_VALUE)  
            .header("Authorization", memberId)  
        .when()  
            .get("/test/{memberId}", memberId)  
        .then()  
            .log().all()  
        .extract();  
    }  
}

이렇게 준비한 테스트를 수행하면 결과는 초록불이 뜨며 아래와 같이 로그가 출력 된다.
이러한 방법으로 MockRest Assured를 이용하여 Controller Unit Test를 수행할 수 있다.

테스트 결과

19:34:38.066 [main] DEBUG _org.springframework.web.servlet.HandlerMapping.Mappings - 
    c.e.r.TestController:
    {GET [/test/{memberId}]}: getTestString(Long)
19:34:38.066 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - 1 mappings in org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
19:34:38.070 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
19:34:38.073 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - ControllerAdvice beans: 1 @ExceptionHandler, 1 ResponseBodyAdvice
19:34:38.073 [main] INFO org.springframework.mock.web.MockServletContext - Initializing Spring TestDispatcherServlet ''
19:34:38.073 [main] INFO org.springframework.test.web.servlet.TestDispatcherServlet - Initializing Servlet ''
19:34:38.073 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected AcceptHeaderLocaleResolver
19:34:38.073 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected FixedThemeResolver
19:34:38.073 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@e7ecd
19:34:38.073 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Detected org.springframework.web.servlet.support.SessionFlashMapManager@e3658c
19:34:38.073 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
19:34:38.073 [main] INFO org.springframework.test.web.servlet.TestDispatcherServlet - Completed initialization in 0 ms
Request method:    GET
Request URI:    http://localhost:8080/test/1
Proxy:            <none>
Request params:    <none>
Query params:    <none>
Form params:    <none>
Path params:    <none>
Headers:        Content-Type=application/json
                Authorization=1
Cookies:        <none>
Multiparts:        <none>
Body:            <none>
19:34:38.076 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - GET "/test/1", parameters={}
19:34:38.076 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped to com.example.restassuredstudy.TestController#getTestString(Long)
19:34:38.077 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor - Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
19:34:38.077 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor - Writing ["test String 1"]
19:34:38.077 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Completed 200 OK
200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 13

test String 1
반응형

'공부 기록' 카테고리의 다른 글

[SPRING SECURITY]우아한 멀티 타입 빌더  (0) 2022.10.09
[Network] 액세스 회선  (0) 2022.07.18
[Network] 라우터  (0) 2022.07.07
[Network] 스위칭 허브  (0) 2022.07.04
[Network] LAN 케이블  (0) 2022.07.03

댓글