Как написать интеграционные тесты на контроллере приложения Spring WebFlux без использования @WebFluxTest - PullRequest
0 голосов
/ 09 апреля 2019

Я хочу написать Integration Test на контроллере моего приложения Spring WebFlux, но я не хочу использовать ни @WebFluxTest, ни @SpringBootTest ...

Почему?

Это потому, что эти аннотации запускают все приложение, которое занимает много секунд, и я просто хочу протестировать контроллер.

Что я хочу сделать, так это написать тесты, которые мы могли бы сделать в классическом Spring Web Application с MockMvc.

Моя проблема в том, что я не знаю, как настроить свой View Resolver, поэтому при запуске теста я получаю эту ошибку:

16:56:35.881 [main] ERROR org.springframework.web.server.adapter.HttpWebHandlerAdapter - [22a6d75c] 500 Server Error for HTTP GET "/product"
java.lang.IllegalStateException: Could not resolve view with name '/product/product'.
    at org.springframework.web.reactive.result.view.ViewResolutionResultHandler.lambda$resolveViews$3(ViewResolutionResultHandler.java:276)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:107)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoCollectList$MonoBufferAllSubscriber.onComplete(MonoCollectList.java:118)
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:360)
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onComplete(FluxConcatMap.java:269)
    at reactor.core.publisher.Operators.complete(Operators.java:131)
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:122)
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
    at reactor.core.publisher.FluxConcatMap.subscribe(FluxConcatMap.java:121)
    at reactor.core.publisher.MonoCollectList.subscribe(MonoCollectList.java:59)
    at reactor.core.publisher.MonoMapFuseable.subscribe(MonoMapFuseable.java:59)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
    at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:147)
    at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
    at reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.java:44)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60)
    at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:241)
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:204)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:204)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoIgnoreThen$ThenAcceptInner.onNext(MonoIgnoreThen.java:296)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:144)
    at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1505)
    at reactor.core.publisher.MonoZip$ZipCoordinator.signal(MonoZip.java:247)
    at reactor.core.publisher.MonoZip$ZipInner.onNext(MonoZip.java:329)
    at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:192)
    at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:92)
    at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
    at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2070)
    at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:1878)
    at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:1752)
    at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:161)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:53)
    at reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.java:44)
    at reactor.core.publisher.MonoDefaultIfEmpty.subscribe(MonoDefaultIfEmpty.java:37)
    at reactor.core.publisher.MonoPeek.subscribe(MonoPeek.java:71)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:128)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60)
    at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
    at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
    at reactor.core.publisher.MonoPeekFuseable.subscribe(MonoPeekFuseable.java:74)
    at reactor.core.publisher.MonoPeekFuseable.subscribe(MonoPeekFuseable.java:74)
    at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
    at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
    at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:275)
    at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:849)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121)
    at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2070)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:162)
    at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:1878)
    at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:1752)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
    at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
    at reactor.core.publisher.MonoMapFuseable.subscribe(MonoMapFuseable.java:59)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:442)
    at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:212)
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:139)
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
    at reactor.core.publisher.FluxConcatMap.subscribe(FluxConcatMap.java:121)
    at reactor.core.publisher.MonoNext.subscribe(MonoNext.java:40)
    at reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.java:44)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60)
    at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60)
    at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
    at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44)
    at reactor.core.publisher.MonoPeekTerminal.subscribe(MonoPeekTerminal.java:61)
    at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172)
    at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.Mono.subscribeWith(Mono.java:3801)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3689)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3656)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3628)
    at org.springframework.test.web.reactive.server.HttpHandlerConnector.lambda$connect$1(HttpHandlerConnector.java:89)
    at org.springframework.mock.http.client.reactive.MockClientHttpRequest.lambda$null$2(MockClientHttpRequest.java:121)
    at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.FluxConcatIterable$ConcatIterableSubscriber.onComplete(FluxConcatIterable.java:146)
    at reactor.core.publisher.FluxConcatIterable.subscribe(FluxConcatIterable.java:60)
    at reactor.core.publisher.MonoIgnoreElements.subscribe(MonoIgnoreElements.java:37)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3695)
    at reactor.core.publisher.Mono.subscribeWith(Mono.java:3801)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3689)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3656)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3628)
    at org.springframework.test.web.reactive.server.HttpHandlerConnector.connect(HttpHandlerConnector.java:100)
    at org.springframework.test.web.reactive.server.WiretapConnector.connect(WiretapConnector.java:71)
    at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:103)
    at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.exchange(DefaultWebClient.java:319)
    at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:283)
    at fr.myclient.product.controller.ProductControllerIntegrationTest.shouldGetProduct(ProductControllerIntegrationTest.java:21)

Поскольку я использую Apache Freemarker, я попытался добавить

@Import({ FreeMarkerAutoConfiguration.class})

Но у меня на самом деле нет контекста Reactive, поэтому все необходимые компоненты не создаются из-за:

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)

Итак, я попытался создать отсутствующие компоненты самостоятельно:

@Bean
public FreeMarkerViewResolver freeMarkerViewResolver(FreeMarkerProperties properties) {
    FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
    resolver.setPrefix(properties.getPrefix());
    resolver.setSuffix(properties.getSuffix());
    resolver.setRequestContextAttribute(properties.getRequestContextAttribute());
    resolver.setViewNames(properties.getViewNames());
    return resolver;
}

@Bean
public ViewResolverRegistry viewResolverRegistry (ApplicationContext applicationContext, FreeMarkerViewResolver freeMarkerViewResolver) {
    ViewResolverRegistry viewResolverRegistry = new ViewResolverRegistry(applicationContext);
    viewResolverRegistry.viewResolver(freeMarkerViewResolver);
    return viewResolverRegistry;
}

@Bean
public WebFluxConfigurationSupport webFluxConfigurationSupport(ViewResolverRegistry viewResolverRegistry) {
    WebFluxConfigurationSupport webFluxConfigurationSupport = new WebFluxConfigurationSupport();
    ReflectionTestUtils.setField(webFluxConfigurationSupport, "viewResolverRegistry", viewResolverRegistry);
    return webFluxConfigurationSupport;
}

Но это не работает ...

Ну, я не понимаю, как я могу просто написать этот тест и сделать его простым ...

Контроллер:

package fr.myclient.product.controller;

import fr.myclient.product.service.ProductService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import reactor.core.publisher.Mono;

@Slf4j
@Controller
@RequestMapping(value = "/product")
@AllArgsConstructor
public class ProductController {

    private ProductService productService;

    @GetMapping
    public Mono<String> getProduct(Model model, @RequestHeader(name = "productId") String productId) {
        return this.productService.getProduct(productId)
                                         .doOnNext(p -> model.addAttribute("product", p))
                                         .thenReturn("/product/product");
    }
}

Интеграционный тест:

package fr.myclient.product.controller;

import fr.myclient.product.config.ControllerTestConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;

@Tag("IntegrationTest")
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ControllerTestConfiguration.class)
public class ProductControllerIntegrationTest {

    @Autowired
    private ProductController productController;

    protected WebTestClient client;

    @BeforeEach
    public void setUp() throws Exception {
        client = WebTestClient.bindToController(productController).build();
    }

    @Test
    public void shouldGetProduct() throws Exception {

        client.get()
              .uri("/product")
              .header("productId", "12345678")
              .accept(MediaType.APPLICATION_JSON_UTF8)
              .exchange()
              .expectStatus()
              .isOk();

    }
}

Конфигурация ControllerTest:

package fr.myclient.product.config;

import fr.myclient.product.model.Product;
import fr.myclient.product.service.ProductService;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

import static org.mockito.Mockito.when;

@Configuration
@ComponentScan(basePackages = "fr.myclient.product.controller")
public class ControllerTestConfiguration {
    @Bean
    public ProductService productService() {

        ProductService mockProductService = Mockito.mock(ProductService.class);

        Product data = Product.builder()
                              .code("12345678")
                              .build();

        Mono<Product> monoProduct = Mono.just(data);
        when(mockProductService.getProduct("12345678")).thenReturn(monoProduct);

        return mockProductService;
    }
}
...