4

I have written a small test on Spring REST docs with custom Jackson module (using Spring Boot 1.3). In my application main class, I only have @SpringBootApplication. I then have another class JacksonCustomizations that looks like this:

@Configuration
public class JacksonCustomizations {

@Bean
public Module myCustomModule() {
    return new MyCustomModule();
}

static class MyCustomModule extends SimpleModule {
    public MyCustomModule() {

        addSerializer(ImmutableEntityId.class, new JsonSerializer<ImmutableEntityId>() {
            @Override
            public void serialize(ImmutableEntityId immutableEntityId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
                jsonGenerator.writeNumber( (Long)immutableEntityId.getId() );
            }
        });
    }
}
}

This customization is picked up perfectly. When I run the Spring Boot application, I see the JSON as it should be.

However, in my documentation test, the customization is not applied. This is the code of my test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration
@WebAppConfiguration
public class NoteControllerDocumentation {

@Rule
public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets");

@Autowired
private WebApplicationContext context;

private MockMvc mockMvc;

@Before
public void setUp() throws Exception {
    mockMvc = MockMvcBuilders.webAppContextSetup(context)
                             .apply(documentationConfiguration(restDocumentation))
                             .build();

}


@Test
public void notesListExample() throws Exception {
    mockMvc.perform(get("/api/notes/"))
           .andExpect(status().isOk())
           .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
           .andDo(document("notes-list-example", responseFields(
                   fieldWithPath("[]").description("An array of <<note-example,note>>s."))));
}

@Configuration
@EnableWebMvc
@Import(JacksonCustomizations.class)
public static class TestConfiguration {
    @Bean
    public NoteController noteController() {
        return new NoteController();
    }
}

}

Note how the application context in my test imports the JacksonCustomizations configuration.

Other things I found:

  • Adding @EnableWebMvc on my boot application stops the customization from working.
  • Removing the @EnableWebMvc on my test stops JSON being produced.
Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
Wim Deblauwe
  • 25,113
  • 20
  • 133
  • 211
  • Did you try adding @Import(JacksonCustomizations.class) to NoteControllerDocumentation class ? – reos Dec 08 '15 at 13:38
  • @reos does not make a difference – Wim Deblauwe Dec 08 '15 at 14:08
  • It sounds like Spring Boot hasn't auto-configured the `ObjectMapper` with your custom `Module`. That matches the fact that you haven't provided `SpringApplicationConfiguration` with the class(es) for your application so Spring Boot doesn't know what to do. In fact, the code that you've shared fails with an `IllegalStateException` that says "No configuration classes or locations found in @SpringApplicationConfiguration" because of this. Can you share a small sample project that contains the exact code that you're running to trigger the problem? – Andy Wilkinson Dec 08 '15 at 17:56
  • Doesn't `ContextConfiguration` uses the inner class `Configuration` automatically? Please download the sample project from https://dl.dropboxusercontent.com/u/6373261/so-34156932.zip which shows the problem. – Wim Deblauwe Dec 09 '15 at 07:54
  • Aah, it wasn't clear (the formatting's a bit off) that `TestConfiguration` was an inner class. Thanks for the sample project. I've posted an answer below. – Andy Wilkinson Dec 09 '15 at 10:18

1 Answers1

2

NoteControllerDocumentation isn't configured to use Spring Boot to create the application context. This means that Spring Boot's auto-configuration doesn't run and, therefore, your custom Jackson module isn't applied to the ObjectMapper.

The simplest solution to your problem is to delete your TestConfiguration class and update SpringApplicationConfiguration to reference DemoApplication instead. That'll leave you with the following code:

package com.example.controller;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentation;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.example.DemoApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
@WebAppConfiguration
public class NoteControllerDocumentation {

    @Rule
    public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets");

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                                 .apply(documentationConfiguration(restDocumentation))
                                 .build();

    }

    @Test
    public void notesListExample() throws Exception {
        mockMvc.perform(get("/api/notes/"))
               .andExpect(status().isOk())
               .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
               .andExpect(content().json("[{\"id\":1}]"))
               .andDo(print())
               .andDo(document("nodes-list-example", responseFields(
                       fieldWithPath("[]").description("An array of <<note-example,note>>s."))));
    }

}

Alternatively, if you want more control over how your control is created (to inject a mock service, for example), you can use a custom configuration class. The key is to annotate that class with @EnableAutoConfiguration so that Spring Boot's auto-configuration is enabled and the customization of the ObjectMapper is performed. This approach will leave you with the following code:

package com.example.controller;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentation;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.example.JacksonCustomizations;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration
@WebAppConfiguration
public class NoteControllerDocumentation {

    @Rule
    public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets");

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                                 .apply(documentationConfiguration(restDocumentation))
                                 .build();

    }

    @Test
    public void notesListExample() throws Exception {
        mockMvc.perform(get("/api/notes/"))
               .andExpect(status().isOk())
               .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
               .andExpect(content().json("[{\"id\":1}]"))
               .andDo(print())
               .andDo(document("nodes-list-example", responseFields(
                       fieldWithPath("[]").description("An array of <<note-example,note>>s."))));
    }

    @Configuration
    @EnableAutoConfiguration
    @Import(JacksonCustomizations.class)
    static class TestConfiguration {

        @Bean
        public NoteController notesController() {
            return new NoteController();
        }

    }

}
Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
  • But what if my controller uses an autowired service or repository? How can I setup a mock for it? Referencing `DemoApplication` would inject the real services/repos. – Wim Deblauwe Dec 09 '15 at 10:35
  • You didn't mention that requirement in the question so I didn't address it. I've updated my answer with details of another approach. – Andy Wilkinson Dec 09 '15 at 14:11