3

I am writing an app using Spring Boot 2.0.1 and WebFlux router functions (not annotation based!). For some of my data objects I have written custom serializers that extend StdSerializer. These I register in a SimpleModule and expose that module as a bean.

This setup works like a charm when I run the application. The bean is instantiated and the REST responses are serialized using the correct serializer.

Now I want to write a test that verifies that the router functions and the handlers behind them work as expected. The services behind the handlers I want to mock. However, in the tests the REST response uses the default serializers.

I have created a small demo project that reproduces the issue. Full code can be found here: http://s000.tinyupload.com/?file_id=82815835861287011625

The Gradle config loads Spring Boot and a few dependencies to support WebFlux and testing.

import io.spring.gradle.dependencymanagement.DependencyManagementPlugin
import org.springframework.boot.gradle.plugin.SpringBootPlugin

buildscript {
    ext {
        springBootVersion = '2.0.1.RELEASE'
    }
    repositories {
        mavenCentral()
        // To allow to pull in milestone releases from Spring
        maven { url 'https://repo.spring.io/milestone' }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-plugin:1.0.5.RELEASE")

    }
}

apply plugin: 'java'
apply plugin: SpringBootPlugin
apply plugin: DependencyManagementPlugin


repositories {
    mavenCentral()

    // To allow to pull in milestone releases from Spring
    maven { url 'https://repo.spring.io/milestone' }
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.boot:spring-boot-dependencies:2.0.1.RELEASE'
    }
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-webflux'

    compile 'org.slf4s:slf4s-api_2.12:1.7.25'

    testCompile 'org.springframework.boot:spring-boot-starter-test'
    testCompile 'org.springframework.boot:spring-boot-starter-json'
    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:2.+"
}

The data object has two fields.

package com.example.model;

public class ReverserResult {
    private String originalString;
    private String reversedString;

    // ... constructor, getters
}

The custom serializer renders the data object in a completely different way than the default serializer. The original field names disappear, the content of the data object is condensed into a single string.

@Component
public class ReverserResultSerializer extends StdSerializer<ReverserResult> {
    // ... Constructor ...

    @Override
    public void serialize(ReverserResult value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeFieldName("result");
        gen.writeString(value.getOriginalString() + "|" + value.getReversedString());
        gen.writeEndObject();
    }
}

The serializer is wrapped in a Jackson module and exposed as bean. This bean is properly picked up and added to the ObjectMapper when running the actual app.

@Configuration
public class SerializerConfig {
    @Bean
    @Autowired public Module specificSerializers(ReverserResultSerializer reverserResultSerializer) {
        SimpleModule serializerModule = new SimpleModule();
        serializerModule.addSerializer(ReverserResult.class, reverserResultSerializer);

        return serializerModule;
    }
}

I have also verified that the bean is actually present in the test. So I can exclude that the context created during testing is missing to load the bean.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ReverserRouteTest {
    @Autowired
    public ReverserRoutes reverserRoutes;

    @MockBean
    public ReverserService mockReverserService;

    @Autowired
    @Qualifier("specificSerializers")
    public Module jacksonModule;

    @Test
    public void testSerializerBeanIsPresent() {
        assertNotNull(jacksonModule);
    }

    @Test
    public void testRouteAcceptsCall() {
        given(mockReverserService.reverse(anyString())).willReturn(new ReverserResult("foo", "bar"));

        WebTestClient client = WebTestClient.bindToRouterFunction(reverserRoutes.createRouterFunction()).build();
        client.get().uri("/reverse/FooBar").exchange().expectStatus().isOk();
    }

    @Test
    public void testRouteReturnsMockedResult() {
        given(mockReverserService.reverse(anyString())).willReturn(new ReverserResult("foo", "bar"));

        WebTestClient client = WebTestClient.bindToRouterFunction(reverserRoutes.createRouterFunction()).build();
        client.get().uri("/reverse/somethingcompletelydifferent")
                .exchange()
                .expectBody().json("{\"result\":\"foo|bar\"}");
    }
}

The result when running the app:

GET http://localhost:9090/reverse/FooBar

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json;charset=UTF-8

{
  "result": "FooBar|raBooF"
}

The result when running the test:

< 200 OK
< Content-Type: [application/json;charset=UTF-8]

{"originalString":"foo","reversedString":"bar"}

I also tried creating my own ObjectMapper instance but it was not used either. I wonder if I am missing a setting (I did try a lot of annotations though...) or if I have hit a bug. I did a lot of searching on Google and SO but none of the solutions I found helped so far. Also, few ppl are using router functions as of now :).

Any help is appreciated!

UPDATE: I tried with 2.0.2.RELEASE and 2.1.0.BUILD-20180509 as well. The result is always the same.

ErosC
  • 453
  • 1
  • 4
  • 11

2 Answers2

5

Instead of creating a WebTestClient manually in the test, you could probably leverage@AutoConfigureWebTestClient and autowire it as following in order to get your Jackson module taken in account properly:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebTestClient
public class ReverserRouteTest {    
    @MockBean
    public ReverserService mockReverserService;

    @Autowired
    @Qualifier("specificSerializers")
    public Module jacksonModule;

    @Autowired
    public WebTestClient client;

    @Test
    public void testSerializerBeanIsPresent() {
        assertNotNull(jacksonModule);
    }

    @Test
    public void testRouteAcceptsCall() {
        given(mockReverserService.reverse(anyString())).willReturn(new ReverserResult("foo", "bar"));

        client.get().uri("/reverse/FooBar").exchange().expectStatus().isOk();
    }

    @Test
    public void testRouteReturnsMockedResult() {
        given(mockReverserService.reverse(anyString())).willReturn(new ReverserResult("foo", "bar"));

        client.get().uri("/reverse/somethingcompletelydifferent")
                .exchange()
                .expectBody().json("{\"result\":\"foo|bar\"}");
    }
}
Sébastien Deleuze
  • 5,950
  • 5
  • 37
  • 38
  • Perfect! That solved the problem! Thanks a lot! I did try with \@WebFluxTest but it never occured to me to combine \@SpringBootTest and \@AutoConfigureWebTestClient. I have published the working code on GitHub, in case anyone wants to have it as example: https://github.com/DerEros/demo-webflux-test-issue/tree/with-autoconfigurewebtestclient – ErosC May 11 '18 at 21:02
2

While the solution presented by Sébastien worked flawlessly in the demo code, I had some issues after introducing it into the main app. @SpringBootTest would pull in too many beans, which in turn would need a lot of external configuration settings, etc. And the test should only cover the routes and the serialization.

Removing @SpringBootTest would however leave me without custom serialization again. So I played a bit with the @AutoConfigure... annotations and found a set that allowed me to test routes and serialization while mocking/omitting everything else.

The full code is available on GitHub https://github.com/DerEros/demo-webflux-test-issue/tree/with-webfluxtest.

The relevant change is here. Hope this is helpful for others as well.

@RunWith(SpringRunner.class)
@WebFluxTest
@AutoConfigureWebClient
@Import({SerializerConfig.class, ReverserResultSerializer.class, ReverserRoutes.class, ReverseHandler.class, ReverserConfig.class})
public class ReverserRouteTest {
    @MockBean
    public ReverserService mockReverserService;

    @Autowired
    @Qualifier("specificSerializers")
    public Module jacksonModule;

    @Autowired
    public WebTestClient client;

    // Tests; no changes here
}
ErosC
  • 453
  • 1
  • 4
  • 11