web-dev-qa-db-ja.com

ユニットテストでSpring WebClientをモックする方法

小さなSpring Boot RESTアプリケーションを作成しました。これは、別のRESTエンドポイントで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がエンドポイントとして使用できるモックサーバーを起動するエレガントな(リアクティブ)方法はありますか?

18
Roman

OkHttpチームが MockWebServer を使用できます。基本的に、Springチームはテストにも使用します(少なくとも here と言いました)。これは ブログ投稿 のコードを使用した例です。

次のサービスがあると考えてみましょう

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}")
        );
        SimpleResponseDto response = apiCaller.callApi().block();
        assertThat(response, is(not(nullValue())));
        assertThat(response.getY(), is("value for y"));
        assertThat(response.getZ(), is(789));

        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))
        )));
    }
}
5

次のメソッドを使用すると、このような呼び出しのためにWebClientをMockitoでモックすることができました。

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;
}
4
Igors Sakels

これに対する組み込みのスプリングサポートはまだ進行中だと思います- https://jira.spring.io/browse/SPR-15286

wiremock がこのようなシナリオを(統合-)テストするのが本当に好きです。特に、これでシリアル化と逆シリアル化全体をテストするためです。 wiremockを使用すると、事前定義されたスタブを使用してリクエストを処理するサーバーを起動します。

2
Mathias Dpunkt

Wire Mocksは統合テストに適していますが、単体テストには必要ないと思います。単体テストの実行中、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でpost要求を実行し、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」ステートメントは、リクエスト本文を除くすべてのパラメーターをテストすることに注意してください。パラメーターの1つが一致しない場合でも、単体テストは失敗し、それによってこれらすべてがアサートされます。次に、「Mono」を同一視できないため、別の検証と要求で要求本文がアサートされます。その後、ステップ検証ツールを使用して結果が検証されます。

そして、他の回答で述べたように、ワイヤモックとの統合テストを実行して、このクラスが適切にワイヤリングされているかどうかを確認し、目的のボディなどでエンドポイントを呼び出します。

0