반응형

Embedded 방식으로 사용하기

build.gradle.kts

dependencies {
    // ...
    // https://central.sonatype.com/artifact/org.wiremock/wiremock-standalone
    implementation("org.wiremock:wiremock-standalone:3.9.1")
}

WireMockConfig

@Configuration
public class WireMockConfig {
    private final Logger log = LoggerFactory.getLogger(WireMockConfig.class);

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer wireMockServer() throws IOException {
        log.info("[WIRE_MOCK_CONFIG] start");

        WireMockServer wireMockServer = new WireMockServer(
            WireMockConfiguration.options()
                .disableRequestJournal()
                .asynchronousResponseEnabled(true)
                .port(19090)
        );

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("wiremock/**/*.json");

        for (Resource resource : resources) {
            log.info("[WIRE_MOCK_CONFIG] jsonFilePath : {}", resource.getURL().getPath());
            wireMockServer.addStubMapping(StubMapping.buildFrom(IOUtils.toString(resource.getInputStream())));
        }

        log.info("[WIRE_MOCK_CONFIG] finish");
        return wireMockServer;
    }
}

Mocking

JavaConfig를 활용한 방법

@RequiredArgsConstructor
@Configuration
public class DemoApiMocking {
    private final WireMockServer wireMockServer;

    @PostConstruct
    public void mocking() {
        wireMockServer.stubFor(
            WireMock.post(WireMock.urlPathMatching("/api/demo/(.*?)"))
                .withRequestBody(WireMock.equalToJson("{\"name\":  \"john\"}"))
                .willReturn(
                    WireMock.aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", MediaTypes.APPLICATION_JSON)
                        .withBody("{\"message\": \"Hi, john\"}")
                )
        );
    }
}

json 파일을 활용한 방법

  • 경로 : ~/src/main/resources/wiremock/demo-api-mocking.json
    {
      "request": {
        "urlPathPattern": "/api/demo/(.*?)",
        "method": "POST",
        "bodyPatterns": [
          {
            "equalToJson": {
              "name": "john"
            }
          }
        ]
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "jsonBody": {
          "message": "Hi, john"
        }
      }
    }

Test

RestTemplate restTemplate = new RestTemplate();

ResponseEntity<String> result = restTemplate.exchange("http://localhost:19090/api/demo/john", HttpMethod.POST, new HttpEntity<>("{\"name\": \"john\"}"), String.class);
String body = result.getBody();

System.out.println(body); // {"message": "Hi, john"}

참고

테스트 케이스에서 사용하기

src/test/resources/application-unittest.properties

demo.url=http://localhost:${wiremock.server.port}

Java 코드로 모킹하여 테스트

WireMockTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("unittest")
public class WireMockTest {
    @Value("${demo.url}")
    private String demoUrl;

    @Test
    public void demo() throws Exception {
//        WireMock.stubFor(
//            WireMock.get(WireMock.urlPathEqualTo("/api/v1/demo"))
//                .willReturn(WireMock.aResponse()
//                    .withStatus(HttpStatus.OK.value())
//                    .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
//                    .withBody(Files.readString(ResourceUtils.getFile("classpath:payload/demo-body.json").toPath()))
//                )
//        );

        WireMock.stubFor(
            WireMock.get(WireMock.urlPathEqualTo("/api/v1/demo"))
                .willReturn(WireMock.aResponse()
                    .withStatus(HttpStatus.OK.value())
                    .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                    .withBody("{\"name\":\"tyler\", \"age\": 50}")
                )
        );

        RestTemplate restTemplate = new RestTemplate();
        Map result = restTemplate.getForObject(demoUrl + "/api/v1/demo", Map.class);

        System.out.println(result.get("name")); // tyler
        System.out.println(result.get("age")); // 50
    }
}

json 파일로 모킹하여 테스트

src/test/resources/stubs/demo-v1.json

{
  "request": {
    "method": "GET",
    "url": "/api/v1/demo"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "name": "tyler",
      "age": 50
    }
  }
}

WireMockTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0, stubs = "classpath:/stubs")
@ActiveProfiles("unittest")
public class WireMockTest {
    @Value("${demo.url}")
    private String demoUrl;

    @Test
    public void demo() {
        RestTemplate restTemplate = new RestTemplate();
        Map result = restTemplate.getForObject(demoUrl + "/api/v1/demo", Map.class);

        System.out.println(result.get("name")); // tyler
        System.out.println(result.get("age")); // 50
    }
}

주요 기능

request body 매칭(json)

demo.json

{
  "request": {
    "urlPathPattern": "/api/demo/(.*?)",
    "method": "POST",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.student.name"
      }
    ]
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "message": "Hi, john"
    }
  }
}

command

curl -X POST --location "http://localhost:19090/api/demo/abcde" \
    -H "Content-Type: application/json" \
    -d "{ \"student\": { \"name\": \"john\" } }"

request header 매칭

demo.json

{
  "request": {
    "urlPathPattern": "/api/demo/(.*?)",
    "method": "POST",
    "headers": {
      "X-TX-ID": {
        "equalTo": "tx-1"
      }
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "message": "Hi, john"
    }
  }
}

command

curl -X POST --location "http://localhost:19090/api/demo/abcde" \
    -H "Content-Type: application/json" \
    -H "X-TX-ID: tx-1"

request header 매칭 (or 조건)

demo.json

{
  "request": {
    "urlPathPattern": "/api/demo/(.*?)",
    "method": "POST",
    "headers": {
      "X-TX-ID": {
        "or": [
          {
            "equalTo": "tx-1"
          },
          {
            "equalTo": "tx-2"
          }
        ]
      }
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "message": "Hi, john"
    }
  }
}

command

curl -X POST --location "http://localhost:19090/api/demo/abcde" \
    -H "Content-Type: application/json" \
    -H "x-tx-id: tx-2"

response 랜덤값 응답하기

WireMockConfig

WireMockConfiguration.options()
    // ...
    .globalTemplating(true) // 추가

demo.json

{
  "request": {
    "urlPathPattern": "/api/demo/(.*?)",
    "method": "POST"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "value1": "{{randomInt lower=5 upper=9}}",
      "value2": "{{randomDecimal lower=-5.0 upper=5.0}}",
      "value3": "{{pickRandom '1' '2' '3'}}"
    }
  }
}

proxy 기능 사용하기

demo.json

{
  "request": {
    "urlPattern": "/proxy-demo/(.*?)"
  },
  "response": {
    "proxyBaseUrl": "https://jsonplaceholder.typicode.com",
    "proxyUrlPrefixToRemove": "/proxy-demo"
  }
}

command

curl -X GET --location "http://localhost:19090/proxy-demo/todos/1"

request 조작하기

DemoRequestFilter

public class DemoRequestFilter extends StubRequestFilter {
    @Override
    public RequestFilterAction filter(Request request) {
        if (request.getUrl().startsWith("/proxy-demo/posts/")) {
            Request newRequest = RequestWrapper.create()
                .transformAbsoluteUrl(url -> url.replace("/posts/1", "/posts/2"))
                .transformBody(body -> new Body(body.asString().replace("john", "tom")))
                .wrap(request);

            return RequestFilterAction.continueWith(newRequest);
        }

        return RequestFilterAction.continueWith(request);
    }

    @Override
    public String getName() {
        return "demo-filter";
    }
}

WireMockConfig

WireMockServer wireMockServer = new WireMockServer(
    WireMockConfiguration.options()
        .extensions(new DemoRequestFilter())
        // ...
);

demo.json

{
  "request": {
    "urlPattern": "/proxy-demo/(.*?)"
  },
  "response": {
    "proxyBaseUrl": "https://jsonplaceholder.typicode.com",
    "proxyUrlPrefixToRemove": "/proxy-demo"
  }
}

command

curl -X PATCH --location "http://localhost:19090/proxy-demo/posts/1" \
    -H "Content-Type: application/json" \
    -d "{\"title\": \"Hi, john\"}"

response 조작하기

DemoResponseTransformer

public class DemoResponseTransformer extends ResponseTransformer {
    @Override
    public Response transform(Request request, Response response, FileSource files, Parameters parameters) {
        String body = response.getBodyAsString().replace("john", "tom");

        return Response.Builder.like(response)
            .body(body)
            .build();
    }

    @Override
    public String getName() {
        return "demo-transformer";
    }

    @Override
    public boolean applyGlobally() {
        return false;
    }
}

WireMockConfig

WireMockServer wireMockServer = new WireMockServer(
    WireMockConfiguration.options()
        .extensions(new DemoResponseTransformer())
        // ...
);

demo.json

{
  "request": {
    "urlPathPattern": "/api/demo/(.*?)"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "message": "Hi, john"
    },
    "transformers": ["demo-transformer"]
  }
}

command

curl -X GET --location "http://localhost:19090/api/demo/1234"
반응형

'Development > Spring' 카테고리의 다른 글

[Spring] Elasticsearch  (0) 2024.01.01
[Spring] 헥사고날 아키텍처(Hexagonal Architecture)  (2) 2023.11.23
[Spring] SpEL  (0) 2023.08.09
[Spring] actuator  (0) 2021.11.27
[Spring] Scheduler Lock  (0) 2021.11.25

+ Recent posts