아래 내용 보다는 코드를 보고 테스트를 실행하는 게 더 좋다.
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
- HTTP Method(GET, POST, PUT, DELETE 등
- Query parameters
Headers
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 |
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/
wiremock :
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 사용 방법 **/
'Spring 스프링 ' 카테고리의 다른 글
Spring boot + Gradle 환경에서 Kotlin 사용해보기 (1) | 2019.04.15 |
---|---|
Spring boot + JPA + Open JDK 사용시 Error creating bean with name entityManagerFactory 해결 (0) | 2019.04.10 |
[스프링/Spring] MockMvc 테스트 (1) | 2018.11.27 |
[스프링/Spring] JUnit Test (0) | 2018.11.23 |
[스프링/Spring] 어노테이션을 이용한 의존 자동 주입 (0) | 2018.10.21 |