source

Spring WebFlux WebClient를 조롱하는 방법은 무엇입니까?

ittop 2023. 7. 26. 22:26
반응형

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.whenMockito.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-runnerWiremock을 사용하여 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

반응형