You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用WireMock测试WebClient?分页删除逻辑测试异常求助

问题:分页数据删除逻辑的WireMock测试失败,删除端点未触发

我实现了一段逻辑:通过client1获取指定客户的分页数据,再调用client2的端点删除这些数据。但用WireMock测试时发现删除端点从未被触发,调试后发现代码根本没走到调用删除端点的部分,不确定是不是WireMock的测试方式有问题,附上实现代码和测试代码,求解决办法。

实现代码

public void deleteItems(String id) {
    client1.get()
        .uri(urlBuilder -> urlBuilder
            .path(CLIENT_ONE_ENDPOINT)
            .queryParam("email", id)
            .build())
        .retrieve()
        .bodyToMono(Items.class)
        .expand(items -> {
            while (!items.isOnLastPage()) {
                int nextPage = items.getCurrentPage() + 1;
                return client1.get()
                    .uri(builder -> builder
                        .path(CLIENT_ONE_ENDPOINT)
                        .queryParam("email", id)
                        .queryParam("pageNumber", nextPage)
                        .build())
                    .retrieve()
                    .bodyToMono(Items.class);
            }
            return Flux.empty();
        })
        .flatMapIterable(Items::getResults)
        .map(or -> {
            return client2.delete()
                .uri(CLIENT_TWO_ENDPOINT + or.getId())
                .retrieve()
                .bodyToMono(ResponseEntity.class)
                .subscribe();
        })
        .subscribe();
}

WireMock测试代码

@Test
void itemsAreRemoved() {
    String email = "example@example.com";
    String itemId = "001";

    WireMock.configureFor(PORT);
    wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .withHeader("Content-Type", equalTo("application/json"))
        .willReturn(aResponse()
            .withHeader("Content-Type", "application/json")
            .withBody(loadResponse(itemId))
            .withStatus(200)));
    wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .withQueryParam("pageNumber", equalTo("1"))
        .withHeader("Content-Type", equalTo("application/json"))
        .willReturn(aResponse()
            .withHeader("Content-Type", "application/json")
            .withStatus(200)));
    wireMockServer.stubFor(delete(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId)));

    service.deleteItems(email);

    verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email)));
    verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .withQueryParam("pageNumber", equalTo("1")));
    verify(exactly(1), deleteRequestedFor(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId)));

}

问题分析与修改方案

核心问题点

  1. 反应式流的订阅逻辑错误:原代码中map里的删除操作调用了.subscribe(),导致异步操作脱离主数据流,外层的.subscribe()不会等待删除操作完成,测试线程可能直接进入verify环节。
  2. expand的循环逻辑错误:while循环不适合expand操作——expand是每次处理完当前元素后生成下一个元素,while会导致逻辑卡死或只获取一次下一页。
  3. 测试未等待异步操作完成:调用deleteItems后直接执行verify,反应式操作还没执行完就断言,必然失败。
  4. WireMock Stub的匹配条件冗余:GET请求的Stub要求Content-Type头,但实际client1的GET请求可能没带这个头,导致Stub不匹配,根本拿不到数据,自然不会触发删除。

修改后的实现代码

public Mono<Void> deleteItems(String id) {
    return client1.get()
        .uri(urlBuilder -> urlBuilder
            .path(CLIENT_ONE_ENDPOINT)
            .queryParam("email", id)
            .build())
        .retrieve()
        .bodyToMono(Items.class)
        .expand(items -> {
            // 替换while为if,符合expand的工作机制
            if (!items.isOnLastPage()) {
                int nextPage = items.getCurrentPage() + 1;
                return client1.get()
                    .uri(builder -> builder
                        .path(CLIENT_ONE_ENDPOINT)
                        .queryParam("email", id)
                        .queryParam("pageNumber", nextPage)
                        .build())
                    .retrieve()
                    .bodyToMono(Items.class);
            }
            return Mono.empty();
        })
        .flatMapIterable(Items::getResults)
        // 用flatMap替代map,将删除操作融入主数据流,不手动订阅
        .flatMap(or -> client2.delete()
            .uri(CLIENT_TWO_ENDPOINT + or.getId())
            .retrieve()
            .bodyToMono(Void.class))
        // 用then()标记流结束,返回Mono<Void>方便测试等待
        .then();
}

修改后的测试代码

@Test
void itemsAreRemoved() {
    String email = "example@example.com";
    String itemId = "001";

    // 去掉GET请求对Content-Type头的匹配(如果实际请求确实带这个头可以保留)
    wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .willReturn(aResponse()
            .withHeader("Content-Type", "application/json")
            .withBody(loadResponse(itemId))
            .withStatus(200)));
    wireMockServer.stubFor(get(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .withQueryParam("pageNumber", equalTo("1"))
        .willReturn(aResponse()
            .withHeader("Content-Type", "application/json")
            .withStatus(200)));
    // 给删除Stub添加返回状态码,确保client2调用能正常完成
    wireMockServer.stubFor(delete(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId))
        .willReturn(aResponse().withStatus(204)));

    // 调用block()等待反应式流完全执行完毕
    service.deleteItems(email).block();

    verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email)));
    verify(getRequestedFor(urlPathEqualTo(CLIENT_ONE_ENDPOINT))
        .withQueryParam("email", equalTo(email))
        .withQueryParam("pageNumber", equalTo("1")));
    verify(exactly(1), deleteRequestedFor(urlPathEqualTo(CLIENT_TWO_ENDPOINT + itemId)));
}

内容的提问来源于stack exchange,提问作者Minimum

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 03:30:46