Issue
Hi I try to create Tests in my Spring WebFlux application, follow my configs:
@Configuration
public class UserRouter implements IUserRouterDoc {
@Override
@Bean
public RouterFunction<ServerResponse> userRoute(final UserHandler handler){
return RouterFunctions.route(findByIdPredicate(), handler::findById)
.andRoute(updatePredicate(), handler::update)
.andRoute(deletePredicate(), handler::delete)
.andRoute(savePredicate(), handler::save);
}
}
@Component
@AllArgsConstructor
@Slf4j
public class UserHandler {
private final BeanValidationService beanValidationService;
private final UserService userService;
private final UserQueryService userQueryService;
private final UserMapper userMapper;
public Mono<ServerResponse> findById(final ServerRequest request){
return getIdParam(request)
.flatMap(userQueryService::findById)
.map(userMapper::toResponse)
.flatMap(response -> ok()
.contentType(APPLICATION_JSON)
.body(Mono.just(response), UserSingleResponse.class));
}
public Mono<ServerResponse> save(final ServerRequest request){
return request.bodyToMono(UserRequest.class)
.flatMap(user -> beanValidationService.verifyConstraints(user, UserRequest.class.getSimpleName()).then(Mono.just(user)))
.flatMap(user -> userService.save(userMapper.toDocument(user)))
.map(userMapper::toResponse)
.flatMap(response -> created(UriComponentsBuilder.fromPath("/users")
.pathSegment("{id}")
.build(response.id()))
.contentType(APPLICATION_JSON)
.body(Mono.just(response), UserSingleResponse.class));
}
public Mono<ServerResponse> update(final ServerRequest request){
return request.bodyToMono(UserRequest.class)
.flatMap(user -> beanValidationService.verifyConstraints(user, UserRequest.class.getSimpleName()).thenReturn(user))
.zipWhen(user -> getIdParam(request))
.flatMap(tuple -> userService.update(userMapper.toDocument(tuple.getT2(), tuple.getT1())))
.map(userMapper::toResponse)
.flatMap(response -> ok()
.contentType(APPLICATION_JSON)
.body(Mono.just(response), UserSingleResponse.class));
}
public Mono<ServerResponse> delete(final ServerRequest request){
return getIdParam(request)
.flatMap(userService::delete)
.then(noContent().build());
}
private Mono<String> getIdParam(final ServerRequest request){
return Mono.just(new UserIdParam(request.pathVariable("id")))
.flatMap(param -> beanValidationService.verifyConstraints(param, UserIdParam.class.getSimpleName()))
.thenReturn(request.pathVariable("id"));
}
}
@Component
@Order(-2)
@Slf4j
@AllArgsConstructor
public class ApiExceptionHandlerProcessor implements WebExceptionHandler {
// classes with code to deal with each exceptions
private final ConflictHandler conflictHandler;
private final MethodNotAllowHandler methodNotAllowHandler;
private final NotFoundHandler notFoundHandler;
private final ConstraintViolationHandler constraintViolationHandler;
private final BeanValidationHandler beanValidationHandler;
private final ResponseStatusHandler responseStatusHandler;
private final ReactiveCalenderHandler reactiveCalenderHandler;
private final GenericHandler genericHandler;
private final JsonProcessingHandler jsonProcessingHandler;
@Override
public Mono<Void> handle(final ServerWebExchange exchange, final Throwable ex) {
return Mono.error(ex)
.onErrorResume(ConflictException.class, e -> conflictHandler.handlerException(exchange, e))
.onErrorResume(MethodNotAllowedException.class, e -> methodNotAllowHandler.handlerException(exchange, e))
.onErrorResume(NotFoundException.class, e -> notFoundHandler.handlerException(exchange, e))
.onErrorResume(ConstraintViolationException.class, e -> constraintViolationHandler.handlerException(exchange, e))
.onErrorResume(BeanValidationException.class, e -> beanValidationHandler.handlerException(exchange, e))
.onErrorResume(ResponseStatusException.class, e -> responseStatusHandler.handlerException(exchange, e))
.onErrorResume(ReactiveCalendarException.class, e -> reactiveCalenderHandler.handlerException(exchange, e))
.onErrorResume(Exception.class, e -> genericHandler.handlerException(exchange, e))
.onErrorResume(JsonProcessingException.class, e -> jsonProcessingHandler.handlerException(exchange, e))
.then();
}
}
This code worked fine when I start my application, but in tests my ApiExceptionHandlerProcessor is not loaded and errors is handler by spring default classes, this configuration worked if I create controllers using annotations (@RestController, etc), but not work in tests using functional componentes, follow a sample test:
@ActiveProfiles("test")
@ExtendWith({SpringExtension.class, MockitoExtension.class})
@ContextConfiguration(classes = {UserMapperImpl.class, LocalValidatorFactoryBean.class, ApiExceptionHandlerProcessor.class,
ConflictHandler.class, MethodNotAllowHandler.class, NotFoundHandler.class, ConstraintViolationHandler.class,
BeanValidationHandler.class, ResponseStatusHandler.class, ReactiveCalenderHandler.class, GenericHandler.class,
JsonProcessingHandler.class, JacksonConfigStub.class})
public class UserHandlerSaveTest {
@Mock
private BeanValidationService beanValidationService;
@Mock
private UserService userService;
@Mock
private UserQueryService userQueryService;
@Autowired
private UserMapper userMapper;
@Autowired
private SmartValidator smartValidator;
private RequestBuilder<UserSingleResponse> userResponseRequestBuilder;
private RequestBuilder<ProblemResponse> problemResponseRequestBuilder;
@BeforeEach
void setup(){
var handler = new UserHandler(beanValidationService, userService, userQueryService, userMapper);
var routers = new UserRouter().userRoute(handler);
userResponseRequestBuilder = userResponseRequestBuilder(routers, "/users/");
problemResponseRequestBuilder = problemResponseRequestBuilder(routers, "/users/");
}
@Test
void saveTes(){
var request = UserRequestFactoryBot.builder().build();
when(beanValidationService.verifyConstraints(any(Object.class), anyString())).thenReturn(Mono.empty());
when(userService.save(any(UserDocument.class))).thenAnswer(invocation -> {
var document = invocation.getArgument(0, UserDocument.class);
document = document.toBuilder()
.id(ObjectId.get().toString())
.createdAt(OffsetDateTime.now())
.updatedAt(OffsetDateTime.now())
.build();
return Mono.just(document);
});
userResponseRequestBuilder.withUri(UriBuilder::build)
.withBody(request)
.generateRequestWithSimpleBody()
.doPost()
.isHttpStatusIsCreated()
.assertBody(response ->{
assertThat(response).usingRecursiveComparison()
.ignoringFields("id", "createdAt", "updatedAt")
.isEqualTo(request);
});
verify(beanValidationService).verifyConstraints(any(Object.class), anyString());
verify(userService).save(any(UserDocument.class));
}
@Test
void whenRequestHasInvalidDataThenThrowError(){
var request = UserRequestFactoryBot.builder().withLargeEmail().build();
var bindingResult = new BeanPropertyBindingResult(request, UserRequest.class.getSimpleName());
smartValidator.validate(request, bindingResult);
when(beanValidationService.verifyConstraints(any(Object.class), anyString()))
.thenReturn(Mono.error(new BeanValidationException(null)));
problemResponseRequestBuilder.withUri(UriBuilder::build)
.withBody(request)
.generateRequestWithSimpleBody()
.doPost()
.isHttpStatusIsBadRequest();
verify(beanValidationService).verifyConstraints(any(Object.class), anyString());
verify(userService, times(0)).save(any(UserDocument.class));
}
}
when I run my test my ApiExceptionHandlerProcessor not loaded and a spring class org.springframework.web.server.adapter.DefaultServerWebExchange handler my error.
Remeber If I use this exception handler works fine using @RestController (tests and start application) but functional componentes don`t work in tests, but if I start my application a class ApiExceptionHandlerProcessor handler fine
Solution
I found my problem, in setup method I configure my webTestClient with my functional components ( my routes)
void setup(){
var handler = new UserHandler(beanValidationService, userService, userQueryService, userMapper);
var routers = new UserRouter().userRoute(handler);
userResponseRequestBuilder = userResponseRequestBuilder(routers, "/users/");
problemResponseRequestBuilder = problemResponseRequestBuilder(routers, "/users/");
}
I replace this configurarion for this
@BeforeEach
void setup(){
userResponseRequestBuilder = userResponseRequestBuilder(applicationContext, "/users/");
problemResponseRequestBuilder = problemResponseRequestBuilder(applicationContext, "/users/");
}
I inject in my test a ApplicationContext biuld with dependencies setted on @ContextConfiguration, now my tests using. my custom class
[EDITED]
To explain more about my solution, my userResponseRequestBuilder and problemResponseRequestBuilder are utilities methods to make requests, follow more details about the code to explain more about solution:
RequestBuilderInstances.java
/// new code
public static RequestBuilder<UserSingleResponse> userResponseRequestBuilder(final ApplicationContext applicationContext, final String baseUri){
return new RequestBuilder<>(applicationContext, baseUri, UserSingleResponse.class);
}
public static RequestBuilder<ProblemResponse> problemResponseRequestBuilder(final ApplicationContext applicationContext, final String baseUri){
return new RequestBuilder<>(applicationContext, baseUri, ProblemResponse.class);
}
/// old code
public static RequestBuilder<UserSingleResponse> userResponseRequestBuilder(RouterFunction<?> routerFunction, final String baseUri){
return new RequestBuilder<>(routerFunction, baseUri, UserSingleResponse.class);
}
public static RequestBuilder<ProblemResponse> problemResponseRequestBuilder(RouterFunction<?> routerFunction, final String baseUri){
return new RequestBuilder<>(routerFunction, baseUri, ProblemResponse.class);
}
RequestBuilder.java
/// new code
public RequestBuilder(final ApplicationContext applicationContext, final String baseUri, final Class<B> responseClass){
this.responseClass = responseClass;
webTestClient = WebTestClient
.bindToApplicationContext(applicationContext)
.configureClient()
.baseUrl(baseUri)
.responseTimeout(Duration.ofDays(1))
.build();
}
/// old code
public RequestBuilder(final RouterFunction<?> routerFunction, final String baseUri, final Class<B> responseClass){
this.responseClass = responseClass;
webTestClient = WebTestClient
.bindToRouterFunction(routerFunction)
.configureClient()
.baseUrl(baseUri)
.responseTimeout(Duration.ofDays(1))
.build();
}
to fix my problem I create my WebTestClient instance using applicationContext, for more details follow a commit with my fix:
https://github.com/juniorjrjl/reactive-calendar/tree/4ccd3541b16b8486ecbf06db7e5af7252fb85266
Answered By - Jose Luiz Junior
Answer Checked By - Timothy Miller (JavaFixing Admin)