【问题标题】:Inconsistency of behavior between Webflux's WebTestClient integration test and Postman REST callWebflux 的 WebTestClient 集成测试和 Postman REST 调用之间的行为不一致
【发布时间】:2019-09-08 05:46:55
【问题描述】:

我正在努力解决 集成测试普通 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();
}

但是,以下集成测试始终无法重现生产错误!!

这是一个绿条测试:

@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 调用失败时,我不确定为什么我的测试通过了。有人可以建议吗?区别在哪里?

【问题讨论】:

    标签: spring-boot spring-webflux spring-test project-reactor


    【解决方案1】:

    不同之处在于您如何实例化您的Mono。在“真实”示例中,您使用的是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”,因为第一次调用总是消耗流。

    【讨论】:

    • 感谢迈克尔的回复。我理解您的回答,但没有办法模仿测试中的“真实”示例吗?我试过这个:.body(BodyInserters.fromObject(user)) 而不是.body(Mono.just(user), User.class)。我仍然无法从测试中重现错误......更不用担心覆盖相同执行路径的集成测试无法发现或重现生产错误?
    • @balteo 查看更新,您必须模仿 InputStream 行为才能在“离线”测试中重现此行为。问题在于,它正是相同的执行路径,因为Mono 不是以相同的方式创建的。如果您想在上面的示例中实现它,您可以创建一个实用方法,该方法接受一个 User 对象,将其序列化为 JSON,然后基于该 JSON 创建一个 InputStream 以在 Mono.fromSupplier 中使用。当然更冗长,但这意味着您的测试更接近您的真实示例,因此可以捕获这样的案例。
    猜你喜欢
    • 2017-12-21
    • 1970-01-01
    • 1970-01-01
    • 2019-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多