본문으로 바로가기
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.



아래 내용 보다는 코드를 보고 테스트를 실행하는 게 더 좋다.

https://github.com/ko-theo/Spring-WireMock


개발 환경
Spring Boot(v2.2.4), Junit5, Gradle, Java 11, Kotlin,


대부분의 사이트 및 블로그는 Junit4 기준으로 설명이 되어있지만(WireMock Document에서도 Junit4를 기준으로 설명하고 있다.) 현재 사내에서 Junit5를 사용하고 있고, 실제로도 Junit5를 많이 사용하고 있기에 Junit5를 기준으로 설명을 작성하였다.


Wiremock 은 다양한 기능(Proxy, Record and Playback.. 등등) 을 제공하고 있으며, 더 자세한 내용은 사이트 방문을 통하여 보는 것을 추천한다. 작성된 내용은 기본적인 내용 그리고 간단하게 작성된 코드에 Wiremock이 적용된 샘플 예제를 첨부하였다.




WireMock


WireMock은 테스트 목적으로 사용할 수 있는 웹 서버이다. WireMock을 사용하면서 특정 포트로 지정한 규칙과 매칭된 HTTP 요청이 들어오면 지정한 응답을 전송한다. 따라서, 실제 웹 서버가 존재하지 않더라도 HTTP 클라이언트를 테스트할 수가 있다. 


장점


Avoid rate limits and server cost

실제 서버 테스트를 진행할 경우 발생할 수 있는 서버 비용 및 제한된 조건을 피할 수 있다.


Cover edge cases and error scenarios

다양한 테스트 케이스 및 Error 발생시 시나리오를 테스트 해볼 수 있다.


Speed

컴퓨터 내부에 서버를 사용함으로써 테스트에 사용되는 네트워크 속도가 빠르다.


WireMock을 사용하기 앞서, Mock과 Stub 용어와 의미를 아는 것이 좋을 것 같아 간단하게 정리하였다.

테스트 더블이란 테스트를 진행할 때 실제 객체를 대신해서 사용되는 모든 방법들을 호칭하는 것으로 Stub, Mock 또한 테스트 더블에 속한다고 할 수 있다.


Dummy, Spy, Mock, Fake, Stub과 같은 용어들 또한 테스트 더블에 속한다고 할 수 있다.

자세한 내용은 요기 블로그 에 잘 정리되어 있으니 꼭 보는 것을 추천한다.


stub : 테스트에서 호출된 요청에 대한 결과를 미리 준비하여 제공을 한다.

mock : 호출에 대한 기대를 명세하고, 해당 내용에 따라 동작하도록 코드로 작성된 프로그래밍 된 객체(MockMvc)




Stand Alone 적용 방법

WireMock 라이브러리 .jar파일만 가지고 있어도 별도의 MockServer을 만들 수 있다.

http://wiremock.org/docs/download-and-installation/ 링크에서 jar파일을 다운로드 한다.


1
java -jar wiremock-standalone-2.26.0.jar --verbose




위의 명령어를 실행하면, 해당 jar파일 경로에 __files와 mappings 디렉토리가 생성되며 로컬호스트:8080으로 접속하면 서버가 실행되었음을 알 수 있다.


stub 설정을 진행한다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
{
   "request":{
      "urlPattern":"/product/p0001",
      "method":"GET"
   },
   "response":{
      "status":200,
      "headers":{
         "Content-Type":"application/json"
      },
      "body":"{ \"_id\" : \"p0001\", \"product_display_name\" : \"Brother - MFC-J4320DW Wireless All-In-One Printer - Black/Gray\", \"category_ids\" : [ \"pcmcat247400050001\" ], \"is_active\" : true, \"on_sale\" : true, \"price\" : { \"list_price\" : 149.99, \"sale_price\" : 123.99 }, \"upc\" : \"012502637677\", \"mpn\" : \"MFC-J4320DW\", \"manufacturer\" : \"Brother\", \"product_short_description\" : \"4-in-1 functionalityBuilt-in wireless LAN (802.11b/g/n)Prints up to 20 ISO ppm in black, up to 18 ISO ppm in color (Print speeds vary with use. See mfg. for info on print speeds.)150-sheet tray2.7\\\"\" }"
   }
}



위의 JSON은 /product/p0001을 요청했을 경우, 200Status에 header에는 "application/json", body에는 요청 응답값 정보가 담겨져 있다. product.json으로 파일을 저정한 후, WireMock서버를 다시 실행한다.



위에 json에서 정의한대로 응답받을 수 있다.

jar파일 실행시 command 옵션으로 포트, https 포트 설정, Address 바인딩 등등 다양한 옵션을 정의할 수 있으며, 위의 JSON 예제처럼 1개의 요청이 아닌 여러개의 stub을 등록하여 서버를 세팅할 수 있다. 아래 예제를 보면 2개의 Request와 Response의 응답값을 등록하여 사용하는 것을 볼 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "mappings": [
    {
      "request": {
        "method": "GET",
        "url": "/one"
      },
      "response": {
        "status": 200
      }
    },
    {
      "request": {
        "url": "/two"
      },
      "response": {
        "body": "Updated"
      }
    }
  ]
}






Spring Boot 적용 방법

Spring Boot에서는 spring-cloud-starter-contract-stub-runner 를 ClassPath에 추가함으로써 WireMock를 사용할 수 있다.

Spring initializer에서 WireMock을 검색하면 아래의 라이브러리를 선택할 수 있다.



선택하면 아래와 같이 Gradle에 dependencies를 추가된다.

testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WiremockForDocsTests {
 
    // A service that calls out over HTTP
    @Autowired
    private Service service;
 
    @BeforeEach
    public void setup() {
        this.service.setBase("http://localhost:"
                + this.environment.getProperty("wiremock.server.port"));
    }
 
    // Using the WireMock APIs in the normal way:
    @Test
    public void contextLoads() throws Exception {
        // Stubbing WireMock
        stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
                .withHeader("Content-Type""text/plain").withBody("Hello World!")));
        // We're asserting if WireMock responded properly
        assertThat(this.service.go()).isEqualTo("Hello World!");
    }
}



Stub 서버를 실행할 때 포트를 다르게 하려면 @AutoConfigureWireMock(port = 8090) 과 같이 port를 설정할 수 있다, 만약 랜덤으로 포트를 설정하려면 0으로 설정한다. 


Stub 서버의 포트는 또한 Test Application Context의 wiremock.server.port 프로퍼티로 바인딩 되는데 관련된 내용의 코드는 12Line에서 볼 수 있다.


@AutoConfigureWireMock을 사용하는 것은 WireMockConfiuration을 테스트 환경에 빈으로 설정하는 것과 동일하다.

@AutoConfigureWireMock 을 선언하여 classpath에 Json stub을 등록할 수 있다(default : src/test/resources/mappings).

stub 속성을 사용하여 JSON 위치를 변경할 수 있다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureWireMock(stubs="classpath:/stubs")
public class WiremockImportApplicationTests {
 
    @Autowired
    private Service service;
 
    @Test
    public void contextLoads() throws Exception {
        assertThat(this.service.go()).isEqualTo("Hello World!");
    }
}





WireMock 코드 작성


게시글을 등록, 조회, 게시글의 댓글얼 조회하는 Rest API가 있고, 요청 및 응답 데이터는 아래와 같다.

No

 METHOD

 URL

 Description

 1

 GET

 /post/{id}

 게시물 검색

 2

 GET

 /comments?postId={id}

 게시물에 있는 댓글 검색

 3

 POST

 /post

 게시물 등록


1. 게시글 조회


1
2
3
4
5
6
{
    "userId"1,
    "id"1,
    "title""sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "completed"false
}



2. 댓글 조회


1
2
3
4
5
6
7
8
9
[
    {
        "postId"1,
        "id"1,
        "name""id labore ex et quam laborum",
        "email""Eliseo@gardner.biz",
        "body""laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
    }
]



3. 게시글 등록


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 요청 **/
{
    "userId" : 1,
    "title" : "THEO",
    "body" : "foo"
}
 
/** 응답 **/
{
    "id"101,
    "userId"1,
    "title""THEO",
    "body""foo"
}


 

자바코드"만" 사용해서 테스트 코드 작성

WireMock은 자바 코드로 요청 및 응답에 대한 테스트 데이터를 작성할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class PostServiceTest @Autowired constructor(
    val postService: PostService,
    val postClient: PostClient
) : BaseSpringTest() {
 
    @BeforeEach
    fun init() {
        wireMockServer = WireMockServer()
        wireMockServer.start()
    }
 
    @AfterEach
    fun shutDown() {
        wireMockServer.stop()
    }
 
    @Test
    fun getPost() {
        stubFor(
            WireMock.get(WireMock.urlEqualTo("/posts/1"))
                .willReturn(
                    WireMock.aResponse()
                        .withStatus(HttpStatus.OK)
                        .withHeader("Content-Type""application/json")
                        .withBody(getBody)
                )
        )
 
        assertThat(postService.getPost(1).title).isEqualTo("delectus aut autem")
        verify(getRequestedFor(urlEqualTo("/posts/1")))
    }
 
    @Test
    fun getComments() {
        stubFor(
            WireMock.get(WireMock.urlEqualTo("/comments?postId=1"))
                .willReturn(
                    WireMock.aResponse()
                        .withStatus(HttpStatus.OK)
                        .withHeader("Content-Type""application/json")
                        .withBody(commentBody)
                )
        )
 
        assertThat(postService.getComments(1).size).isEqualTo(1)
    }
 
    @Test
    fun createPost() {
        stubFor(WireMock.post(WireMock.urlEqualTo("/posts"))
                .withRequestBody(
                    equalToJson("{ \"userId\" : 1, \"title\" : \"THEO\", \"body\" : \"GAMEHUB\"}")
                )
                .willReturn(
                    WireMock.aResponse()
                        .withStatus(HttpStatus.OK)
                        .withHeader("Content-Type""application/json")
                        .withBody(postBody)
                )
        )
 
        assertThat(postService.createPost(todoRequest).title).isEqualTo("THEO")
    }
}
 
cs


JSON 파일을 이용한 테스트 코드 작성

위의 예제에서는 stubFor을 이용해서 요청 및 응답 데이터를 작성했다면, 아래 예제는 요청 및 응답 정보를 포함하는  json파일을 이용하여 테스트를 작성한다.

WireMock은 Default로 resources/mappings 디렉토리에 있는 Json 들을 import 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/** getPost **/
{
  "request": {
    "method""GET",
    "url""/posts/1"
  },
  "response": {
    "headers": {
      "Content-Type""application/json"
    },
    "status"200,
    "body""{\"userId\" : \"1\", \"id\" : \"1\", \"title\" : \"delectus aut autem\", \"completed\": \"false\"}"
  }
}
 
/** getComments**/
{
  "request": {
    "method""GET",
    "url""/comments?postId=1"
  },
  "response": {
    "headers": {
      "Content-Type""application/json"
    },
    "status"200,
    "body""[{\"postId\" : \"1\", \"id\" : \"1\", \"name\" : \"id labore ex et quam laborum\", \"email\": \"Eliseo@gardner.biz\", \"body\": \"laudantium enim quasi est quidem magnam voluptate ipsam eos tempora quo necessitatibus dolor quam autem quasi reiciendis et nam sapiente accusantium\"}]"
  }
}
 
/** createPost **/
{
  "request": {
    "method""POST",
    "url""/posts",
    "bodyPatterns" : [
      { "equalToJson" :  "{ \"userId\" : 1, \"title\" : \"THEO\", \"body\" : \"GAMEHUB\"}"}
    ]
  },
  "response": {
    "status":200,
    "headers":{
      "Content-Type":"application/json"
    },
    "body" : "{ \"id\" : \"1\", \"userId\" : \"1\", \"title\" : \"THEO\", \"body\" : \"GAMEHUB\" }"
  }
}
cs


Request Matching


WireMock에서는 Attribute 사용하여 stub 또는 검증을 진행한다.

내용이 너무 많아 중요한 몇개(URL, HTTP Method[GET, POST], Query Parameters, Header, Request Body)만 설명을 진행한다. 

  • URL
  •  java

     json

     Description

     urlEqualTo

     url

     Equality matching on path and query

     urlMatching

     urlPattern

     Regex matching on path and query

     urlPathEqualTo

     urlPath

     Equality matching on the path only

     urlPathMatching urlPathPattern

     Regex matching on the path only


  • HTTP Method(GET, POST, PUT, DELETE 등
  • Query parameters
  • Headers

1
.withHeader("Content-Type", equalTo("application/json"))



1
2
3
4
5
6
7
8
9
10
11
12
{
  "request": {
    ...
    "headers": {
      "Content-Type": {
        "equalTo""application/json"
      }
    }
    ...
  },
  ...
}

  • Request body
1
.withRequestBody(equalToJson("{ \"total_results\": 4 }"))



1
2
3
4
5
6
7
8
9
10
{
  "request": {
    ...
    "bodyPatterns" : [ {
      "equalToJson" : { "total_results"4 }
    } ]
    ...
  },
  ...
}



다른 속성들은 아래와 같고, 사용 방법은 http://wiremock.org/docs/request-matching/를 확인한다.

  • Basic authentication (a special case of header matching)
  • Cookies
  • Multipart/form-data




검증(Verifying)

WireMock은 수신한 모든 요청을 Memory에 기록하며, 실제 서버에 요청이 되었는지 확인할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun getPost() {
    stubFor(
        WireMock.get(WireMock.urlEqualTo("/posts/1"))
            .willReturn(
                WireMock.aResponse()
                    .withStatus(HttpStatus.OK)
                    .withHeader("Content-Type""application/json")
                    .withBody(getBody)
            )
    )
 
    assertThat(postService.getPost(1).title).isEqualTo("delectus aut autem")
    verify(getRequestedFor(urlEqualTo("/posts/2")))
}
cs




WireMock and Spring MVC Mocks

Spring Cloud Contract에서는 WireMock를 좀 더 편리하게 사용할 수 있는 MockRestServiceServer을 제공한다.

아래는 예제 코드이며, 관련 링크를 첨부한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class WiremockForDocsMockServerApplicationTests {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @Autowired
    private Service service;
 
    @Test
    public void contextLoads() throws Exception {
        // will read stubs classpath
        MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
                .baseUrl("https://example.org").stubs("classpath:/stubs/resource.json")
                .build();
        // We're asserting if WireMock responded properly
        assertThat(this.service.go()).isEqualTo("Hello World");
        server.verify();
    }
 
}
cs



** 참고

json 제공 : https://jsonplaceholder.typicode.com/


stub : https://m.blog.naver.com/PostView.nhn?blogId=suresofttech&logNo=221204092938&proxyReferer=https%3A%2F%2Fwww.google.com%2F


wiremock :

https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-build-tools

https://www.baeldung.com/introduction-to-wiremock

http://wiremock.org/docs/getting-started/

https://www.youtube.com/watch?v=pPtTz6X0yRE&t=3s

https://dzone.com/articles/mocking-rest-api-with-wiremock-using-recording-mod

/** Stand alone 사용 방법 **/

https://dzone.com/articles/wiremock-mock-your-rest-apis