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",因为第一次调用总是消耗流。
我正在努力解决集成测试和普通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",因为第一次调用总是消耗流。