Webflux 的 WebTestClient 集成测试与 Postman REST 调用之间的行为不一致

Inconsistency of behavior between Webflux's WebTestClient integration test and Postman REST call

我正在努力解决集成测试普通REST调用之间的行为不一致 ].

让我解释一下:我的生产代码中有一个错误导致:当我从休息客户端(例如 Postman)执行 POST 时出现 NoSuchElementException: Source was empty 异常。

我试图重复使用已订阅的 Mono。见下文:

public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
    Mono<User> userMono = serverRequest.bodyToMono(User.class);//Can only be subscribed to once!!
    return validateUser(userMono)
        .switchIfEmpty(validateEmailNotExists(userMono))
        .switchIfEmpty(saveUser(userMono))
        .single();
}

但是下面的集成测试一直无法重现生产bug!!

这是绿条测试:

@Test
void shouldSignUpUser() {
    WebTestClient client = WebTestClient
        .bindToRouterFunction(config.route(userHandler))
        .build();

    User user = User.builder()
        .firstName("John")
        .lastName("Smith")
        .email("john@example.com")
        .build();

    client
        .post()
        .uri("/api/user")
        .body(Mono.just(user), User.class)
        .exchange()
        .expectStatus()
        .is2xxSuccessful()
        .expectBody()
        .jsonPath("$.id")
        .isNotEmpty()
        .jsonPath("$.firstName")
        .isEqualTo("John");
}

即使我指定一个完整的网络环境如下:

@SpringBootTest(
    properties = "spring.main.web-application-type=reactive",
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)

我不确定为什么当来自 Postman/curl 的 POST 调用失败时我的测试通过了。有人可以建议吗?哪里不一样了?

区别在于实例化 Mono 的方式。在 "real" 示例中,您使用 serverRequest.bodyToMono(User.class) 它将读取输入流,然后将结果解析为一个对象。当该输入流被消耗并关闭时,其中的数据就消失了——您不能再次打开它并从中获取与之前相同的数据。因此,您无法从同一个 Mono 中获取 User 对象,除非您缓存了它的结果。

Mono.just() 但是 explicitly uses a value that's "captured at instantiation time". 该值,即您在该测试中构建的用户,本质上只是存储在该 Mono 中的常量,因此可以无限期地重播没问题。

作为简化示例,请注意以下几点:

public class DemoApplication {

    static class Foo {

        String bar;

        public String toString() {
            return bar;
        }

    }

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.bar = "hello";
        Mono<Foo> mono = Mono.just(foo);

        mono.subscribe(System.out::println);
        mono.subscribe(System.out::println);
    }

}

...创建 Foo 的新实例并使用 Mono.just。正如预期的那样,我们得到 "hello" 打印两次。

但是,您的实际用例与以下情况更相似:

public class DemoApplication {

    static class Foo {

        String bar;

        public String toString() {
            return bar;
        }

    }

    public static void main(String[] args) {
        InputStream targetStream = new ByteArrayInputStream("{\"bar\":\"hello\"}".getBytes());
        Mono<Foo> mono = Mono.fromSupplier(() -> new Gson().fromJson(new InputStreamReader(targetStream), Foo.class));

        mono.subscribe(System.out::println);
        mono.subscribe(System.out::println);
    }

}

...只会打印一次 "hello",因为第一次调用总是消耗流。