Spring WebFlux WebClient를 조롱하는 방법은 무엇입니까?
우리는 다른 REST 끝점에서 REST 요청을 수행하는 작은 Spring Boot REST 애플리케이션을 작성했습니다.
@RequestMapping("/api/v1")
@SpringBootApplication
@RestController
@Slf4j
public class Application
{
@Autowired
private WebClient webClient;
@RequestMapping(value = "/zyx", method = POST)
@ResponseBody
XyzApiResponse zyx(@RequestBody XyzApiRequest request, @RequestHeader HttpHeaders headers)
{
webClient.post()
.uri("/api/v1/someapi")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(request.getData()))
.exchange()
.subscribeOn(Schedulers.elastic())
.flatMap(response ->
response.bodyToMono(XyzServiceResponse.class).map(r ->
{
if (r != null)
{
r.setStatus(response.statusCode().value());
}
if (!response.statusCode().is2xxSuccessful())
{
throw new ProcessResponseException(
"Bad status response code " + response.statusCode() + "!");
}
return r;
}))
.subscribe(body ->
{
// Do various things
}, throwable ->
{
// This section handles request errors
});
return XyzApiResponse.OK;
}
}
우리는 Spring이 처음이라 이 작은 코드 스니펫에 대한 단위 테스트를 작성하는 데 어려움을 겪고 있습니다.
webClient 자체를 조롱하거나 webClient가 엔드포인트로 사용할 수 있는 모의 서버를 시작할 수 있는 우아한(반응적) 방법이 있습니까?
▁a▁custom▁providing다▁by▁we▁thisished니▁accompl습했성달사를 제공함으로써 달성했습니다.ExchangeFunction
단순히 우리가 원하는 응답을 돌려주는 것.WebClientBuilder
:
webClient = WebClient.builder()
.exchangeFunction(clientRequest ->
Mono.just(ClientResponse.create(HttpStatus.OK)
.header("content-type", "application/json")
.body("{ \"key\" : \"value\"}")
.build())
).build();
myHttpService = new MyHttpService(webClient);
Map<String, String> result = myHttpService.callService().block();
// Do assertions here
Mokcito를 사용하여 호출이 이루어졌는지 확인하거나 클래스의 여러 장치 테스트에서 WebClient를 재사용하려면 교환 기능을 조롱할 수도 있습니다.
@Mock
private ExchangeFunction exchangeFunction;
@BeforeEach
void init() {
WebClient webClient = WebClient.builder()
.exchangeFunction(exchangeFunction)
.build();
myHttpService = new MyHttpService(webClient);
}
@Test
void callService() {
when(exchangeFunction.exchange(any(ClientRequest.class)))
.thenReturn(buildMockResponse());
Map<String, String> result = myHttpService.callService().block();
verify(exchangeFunction).exchange(any());
// Do assertions here
}
와 : " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " "when
수 .Mono.when
에 Mockito.when
.
출처:
다음과 같은 방법으로 Mockito를 사용하여 WebClient를 조롱할 수 있습니다.
webClient
.get()
.uri(url)
.header(headerName, headerValue)
.retrieve()
.bodyToMono(String.class);
또는
webClient
.get()
.uri(url)
.headers(hs -> hs.addAll(headers));
.retrieve()
.bodyToMono(String.class);
모의 방법:
private static WebClient getWebClientMock(final String resp) {
final var mock = Mockito.mock(WebClient.class);
final var uriSpecMock = Mockito.mock(WebClient.RequestHeadersUriSpec.class);
final var headersSpecMock = Mockito.mock(WebClient.RequestHeadersSpec.class);
final var responseSpecMock = Mockito.mock(WebClient.ResponseSpec.class);
when(mock.get()).thenReturn(uriSpecMock);
when(uriSpecMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(headersSpecMock);
when(headersSpecMock.header(notNull(), notNull())).thenReturn(headersSpecMock);
when(headersSpecMock.headers(notNull())).thenReturn(headersSpecMock);
when(headersSpecMock.retrieve()).thenReturn(responseSpecMock);
when(responseSpecMock.bodyToMono(ArgumentMatchers.<Class<String>>notNull()))
.thenReturn(Mono.just(resp));
return mock;
}
OkHttp 팀에서 MockWebServer를 사용할 수 있습니다.기본적으로 스프링 팀은 테스트에도 사용합니다(적어도 여기서 말한 방법).다음은 소스와 관련된 예입니다.
Tim의 블로그 게시물에 따르면 다음과 같은 서비스를 제공한다고 합니다.
class ApiCaller { private WebClient webClient; ApiCaller(WebClient webClient) { this.webClient = webClient; } Mono<SimpleResponseDto> callApi() { return webClient.put() .uri("/api/resource") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "customAuth") .syncBody(new SimpleRequestDto()) .retrieve() .bodyToMono(SimpleResponseDto.class); } }
그런 다음 테스트를 다음과 같은 방식으로 설계할 수 있습니다(원래와 비교하여 원자로에서 비동기 체인을 테스트하는 방법을 변경했습니다).
class ApiCallerTest { private final MockWebServer mockWebServer = new MockWebServer(); private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString())); @AfterEach void tearDown() throws IOException { mockWebServer.shutdown(); } @Test void call() throws InterruptedException { mockWebServer.enqueue(new MockResponse().setResponseCode(200) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .setBody("{\"y\": \"value for y\", \"z\": 789}") ); //Asserting response StepVerifier.create(apiCaller.callApi()) .assertNext(res -> { assertNotNull(res); assertEquals("value for y", res.getY()); assertEquals("789", res.getZ()); }) .verifyComplete(); //Asserting request RecordedRequest recordedRequest = mockWebServer.takeRequest(); //use method provided by MockWebServer to assert the request header recordedRequest.getHeader("Authorization").equals("customAuth"); DocumentContext context = >JsonPath.parse(recordedRequest.getBody().inputStream()); //use JsonPath library to assert the request body assertThat(context, isJson(allOf( withJsonPath("$.a", is("value1")), withJsonPath("$.b", is(123)) ))); } }
저는 통합 테스트를 위해 WireMock을 사용합니다.OkHttp MockeWebServer보다 훨씬 우수하고 더 많은 기능을 지원한다고 생각합니다.다음은 간단한 예입니다.
public class WireMockTest {
WireMockServer wireMockServer;
WebClient webClient;
@BeforeEach
void setUp() throws Exception {
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
wireMockServer.start();
webClient = WebClient.builder().baseUrl(wireMockServer.baseUrl()).build();
}
@Test
void testWireMock() {
wireMockServer.stubFor(get("/test")
.willReturn(ok("hello")));
String body = webClient.get()
.uri("/test")
.retrieve()
.bodyToMono(String.class)
.block();
assertEquals("hello", body);
}
@AfterEach
void tearDown() throws Exception {
wireMockServer.stop();
}
}
만약 당신이 정말로 그것을 조롱하고 싶다면 나는 JMockit을 추천합니다.전화는 필요 없습니다.when
여러 번 테스트한 코드에서처럼 동일한 통화를 사용할 수 있습니다.
@Test
void testJMockit(@Injectable WebClient webClient) {
new Expectations() {{
webClient.get()
.uri("/test")
.retrieve()
.bodyToMono(String.class);
result = Mono.just("hello");
}};
String body = webClient.get()
.uri(anyString)
.retrieve()
.bodyToMono(String.class)
.block();
assertEquals("hello", body);
}
와이어 모크는 통합 테스트에 적합하지만, 유닛 테스트에는 필요하지 않다고 생각합니다.장치 테스트를 수행하는 동안 WebClient가 원하는 매개 변수로 호출되었는지 여부만 알고 싶습니다.이를 위해 WebClient 인스턴스에 대한 모의실험이 필요합니다.또는 WebClientBuilder를 대신 주입할 수 있습니다.
아래와 같이 포스트 요청을 하는 단순화된 방법을 고려해 보겠습니다.
@Service
@Getter
@Setter
public class RestAdapter {
public static final String BASE_URI = "http://some/uri";
public static final String SUB_URI = "some/endpoint";
@Autowired
private WebClient.Builder webClientBuilder;
private WebClient webClient;
@PostConstruct
protected void initialize() {
webClient = webClientBuilder.baseUrl(BASE_URI).build();
}
public Mono<String> createSomething(String jsonDetails) {
return webClient.post()
.uri(SUB_URI)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(jsonDetails), String.class)
.retrieve()
.bodyToMono(String.class);
}
}
createSomething 메서드는 예제의 단순성을 위해 Json으로 가정된 String을 수락하고 URI에서 게시 요청을 수행하고 String으로 가정된 출력 응답 본문을 반환합니다.
이 방법은 StepVerifier를 사용하여 아래와 같이 단위 테스트를 수행할 수 있습니다.
public class RestAdapterTest {
private static final String JSON_INPUT = "{\"name\": \"Test name\"}";
private static final String TEST_ID = "Test Id";
private WebClient.Builder webClientBuilder = mock(WebClient.Builder.class);
private WebClient webClient = mock(WebClient.class);
private RestAdapter adapter = new RestAdapter();
private WebClient.RequestBodyUriSpec requestBodyUriSpec = mock(WebClient.RequestBodyUriSpec.class);
private WebClient.RequestBodySpec requestBodySpec = mock(WebClient.RequestBodySpec.class);
private WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
private WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
@BeforeEach
void setup() {
adapter.setWebClientBuilder(webClientBuilder);
when(webClientBuilder.baseUrl(anyString())).thenReturn(webClientBuilder);
when(webClientBuilder.build()).thenReturn(webClient);
adapter.initialize();
}
@Test
@SuppressWarnings("unchecked")
void createSomething_withSuccessfulDownstreamResponse_shouldReturnCreatedObjectId() {
when(webClient.post()).thenReturn(requestBodyUriSpec);
when(requestBodyUriSpec.uri(RestAdapter.SUB_URI))
.thenReturn(requestBodySpec);
when(requestBodySpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestBodySpec);
when(requestBodySpec.body(any(Mono.class), eq(String.class)))
.thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(TEST_ID));
ArgumentCaptor<Mono<String>> captor
= ArgumentCaptor.forClass(Mono.class);
Mono<String> result = adapter.createSomething(JSON_INPUT);
verify(requestBodySpec).body(captor.capture(), eq(String.class));
Mono<String> testBody = captor.getValue();
assertThat(testBody.block(), equalTo(JSON_INPUT));
StepVerifier
.create(result)
.expectNext(TEST_ID)
.verifyComplete();
}
}
'when' 문은 요청 Body를 제외한 모든 매개 변수를 테스트합니다.매개 변수 중 하나가 일치하지 않더라도 장치 테스트가 실패하여 이 모든 것을 확인할 수 있습니다.그러면 요청 본문은 별도의 검증에서 주장되고 '모노'를 동일시할 수 없기 때문에 주장됩니다.그런 다음 단계 검증기를 사용하여 결과를 검증합니다.
그런 다음 다른 답변에서 언급한 것처럼 와이어 모의를 사용하여 통합 테스트를 수행하여 이 클래스가 올바르게 배선되는지 확인하고 원하는 바디로 끝점을 호출하는 등의 작업을 수행할 수 있습니다.
저는 여기서 이미 제시된 답에 있는 모든 해결책을 시도해 보았습니다.질문에 대한 대답은 다음과 같습니다.유닛 테스트를 수행할지 통합 테스트를 수행할지에 따라 다릅니다.
장치 테스트를 위해 WebClient를 조롱하는 것 자체가 너무 장황하고 코드가 너무 많이 필요합니다.Mocking Exchange 기능은 더 간단하고 쉽습니다.이를 위해 허용된 답변은 @Renette의 솔루션이어야 합니다.
통합 테스트의 경우 OkHttp MockWebServer를 사용하는 것이 가장 좋습니다.신축성 있는 것을 사용하는 것은 간단합니다.서버를 사용하면 유닛 테스트 사례에서 수동으로 처리해야 하는 일부 오류 사례를 처리할 수 있습니다.
와 함께spring-cloud-starter-contract-stub-runner
Wiremock을 사용하여 API 응답을 조롱할 수 있습니다.여기서 제가 매체에 대해 설명한 작업 예를 찾을 수 있습니다.AutoConfigureMockMvc
위치 "Wiremock" 위치: /mappings 위치)에 있는 모든 내용을 합니다.src/test/resources/mappings
디스크에 저장).
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
class BalanceServiceTest {
private static final Logger log = LoggerFactory.getLogger(BalanceServiceTest.class);
@Autowired
private BalanceService service;
@Test
public void test() throws Exception {
assertNotNull(service.getBalance("123")
.get());
}
}
다음은 매핑 파일의 모양에 대한 예입니다.balance.json
파일에는 필요한 모든 json 컨텐츠가 포함되어 있습니다.정적 구성 파일 또는 프로그램에서 응답 지연 또는 실패를 모방할 수도 있습니다.그들의 웹사이트에 더 많은 정보.
{
"request": {
"method": "GET",
"url": "/v2/accounts/123/balance"
},
"response": {
"status": 200,
"delayDistribution": {
"type": "lognormal",
"median": 1000,
"sigma": 0.4
},
"headers": {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
"bodyFileName": "balance.json"
}
}
저는 webclient를 유닛 테스트에 사용하고 싶었지만 mockito가 너무 복잡해서 mockito를 설정할 수 없어서 유닛 테스트에서 mockit webclient를 구축할 수 있는 라이브러리를 만들었습니다.또한 응답을 발송하기 전에 URL, 메서드, 헤더 및 요청 본문을 확인합니다.
FakeWebClientBuilder fakeWebClientBuilder = FakeWebClientBuilder.useDefaultWebClientBuilder();
FakeRequestResponse fakeRequestResponse = new FakeRequestResponseBuilder()
.withRequestUrl("https://google.com/foo")
.withRequestMethod(HttpMethod.POST)
.withRequestBody(BodyInserters.fromFormData("foo", "bar"))
.replyWithResponse("test")
.replyWithResponseStatusCode(200)
.build();
WebClient client =
FakeWebClientBuilder.useDefaultWebClientBuilder()
.baseUrl("https://google.com")
.addRequestResponse(fakeRequestResponse)
.build();
// Our webclient will return `test` when called.
// This assertion would check if all our enqueued responses are dequeued by the class or method we intend to test.
Assertions.assertTrue(fakeWebClientBuilder.assertAllResponsesDispatched());
조롱보다는 Okhttp MockWebServer를 사용하는 것을 강력히 추천합니다.MockWebServer가 되는 이유는 훨씬 더 깨끗한 접근 방식입니다.
다음은 Web Client를 테스트하는 장치에 사용할 수 있는 코드 템플릿입니다.
class Test {
private ClassUnderTest classUnderTest;
public static MockWebServer mockWebServer;
@BeforeAll
static void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@BeforeEach
void initialize() {
var httpUrl = mockWebServer.url("/xyz");
var webClient = WebClient.create(httpUrl.toString());
classUnderTest = new ClassUnderTest(webClient);
}
@Test
void testMehod() {
var mockResp = new MockResponse();
mockResp.setResponseCode(200);
mockResp.addHeader("Content-Type", "application/json");
mockResp.setBody(
"{\"prop\":\"some value\"}");
mockWebServer.enqueue(mockResp);
// This enqueued response will be returned when webclient is invoked
...
...
classUnderTest.methodThatInvkesWebClient();
...
...
}
@AfterAll
static void tearDown() throws IOException {
mockWebServer.shutdown();
}
}
특히 주의를 기울입니다.initialize
방법.여기서 까다로운 것은 그것뿐입니다.
경로./xyz
기본 URL이 아니라 리소스 경로입니다.기본 URL을 MockWebServer에 알려줄 필요는 없습니다.MockWebServer는 임의 포트가 있는 로컬 호스트의 서버를 스핀업합니다.그리고 자신의 기본 URL을 제공하면 유닛 테스트가 실패합니다.
mockWebServer.url("/xyz")
그러면 MockWebServer가 수신 중인 호스트 및 포트와 리소스 경로 등의 기본 URL이 제공됩니다.localhost:8999/xyz
이 URL로 WebClient를 만들어야 합니다.
WebClient.create(httpUrl.toString())
그러면 장치 테스트를 위해 MockWebServer로 호출하는 WebClient가 생성됩니다.
언급URL : https://stackoverflow.com/questions/45301220/how-to-mock-spring-webflux-webclient
'source' 카테고리의 다른 글
Windows에서 GTK+ 3.0을 어떻게 설치합니까? (0) | 2023.07.26 |
---|---|
Ajax.ActionLink가 작동하지 않습니다. 응답.IsAjaxRequest()는 항상 false입니다. (0) | 2023.07.26 |
PL/SQL 대량 수집을 스파스 키를 사용하여 연관 배열로 수집 (0) | 2023.07.26 |
jQuery AJAX vs.업데이트 패널 (0) | 2023.07.26 |
mysql에서 '보기에서 표 만들기' 구문이 쉽습니까? (0) | 2023.07.26 |