Несоответствие поведения между интеграционным тестом Webflux WebTestClient и вызовом Postman REST

Я борюсь с поведением несоответствия между интеграционным тестом и простым вызовом REST.

Позвольте мне объяснить: у меня была ошибка в моем производственном коде, вызывающая исключение: NoSuchElementException: Source was empty, когда я выполняю POST из клиента отдыха (например, Postman).

Я пытался повторно использовать 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("[email protected]")
        .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
)

Я не уверен, почему мой тест проходит, когда не удается выполнить вызов POST от Postman / curl. Может кто-нибудь посоветовать? В чем разница?


person balteo    schedule 08.09.2019    source источник


Ответы (1)


Разница в том, как вы создаете экземпляр своего Mono. В «реальном» примере вы используете serverRequest.bodyToMono(User.class), который будет читать входной поток и затем преобразовывать результат в объект. Когда этот входной поток потребляется и закрывается, данные в нем исчезают - вы не можете просто открыть его снова и получить из него те же данные, что и раньше. Следовательно, вы не можете получить User объект из того же Mono, если вы не кэшируете его результат.

Mono.just() однако явно использует значение, которое "захвачено во время создания". Это значение, пользователь, которого вы создали в этом тесте, по сути, является просто константой, хранящейся в этом 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. Как и ожидалось, мы получаем «привет» дважды.

Однако ваш реальный вариант использования гораздо больше похож на следующий:

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

}

... который будет печатать "привет" только один раз, поскольку первый вызов всегда использует поток.

person Michael Berry    schedule 08.09.2019
comment
Спасибо за этот ответ, Майкл. Я понимаю ваш ответ, но нельзя ли воспроизвести реальный пример в тесте? Я пробовал это: .body(BodyInserters.fromObject(user)) вместо .body(Mono.just(user), User.class). Тем не менее я не могу воспроизвести ошибку из теста ... Более того, меня не беспокоит, что производственная ошибка не может быть обнаружена или воспроизведена интеграционным тестом, охватывающим тот же самый путь выполнения? - person balteo; 08.09.2019
comment
@balteo Посмотрите обновление, вам придется имитировать поведение InputStream, чтобы воспроизвести это поведение в автономном тесте. Проблема в том, что это точно не один и тот же путь выполнения, поскольку Mono создается по-разному. Если вы хотите реализовать это в приведенном выше примере, вы можете создать служебный метод, который принимает объект User, сериализует его в JSON, а затем создает InputStream на основе этого JSON для использования в Mono.fromSupplier. Более подробный, конечно, но это означает, что ваш тест намного ближе к вашему реальному примеру, поэтому вы можете поймать такие случаи. - person Michael Berry; 08.09.2019