javaspring-bootjackson

Spring Boot tests and Jackson - Get an instance of ObjectMapper from context settings (application.yml) in tests without overhead config


I want to know what is the best way to get an instance of ObjectMapper from context settings in application.yml without "overhead" configuration(which means, WebAppConfiguration and ContextConfiguration) in Spring Boot, with only the minimized number of classes to load in the tests.

I already know that, to load Jackson-related config in application.yml(spring.jackson.xxx), you must @Autowired a Jackson2ObjectMapperBuilder, and use builder.build() to get an ObjectMapper.

So I create a configuration class:

@Configuration
public class JacksonConfig {
    @Autowired
    private Jackson2ObjectMapperBuilder objectMapperBuilder;

    @Bean
    public ObjectMapper objectMapper() {
        return objectMapperBuilder.build();
    }
}

And a wrapper class:

@Service
@Slf4j
public class JsonConverter {

    @Getter
    @Autowired
    private ObjectMapper mapper;

    public String objectToJson(Object obj) {
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("Cannot serialize object {} to JSON", obj);
            throw new ServiceException(ErrorCode.JSON_CONVERTER_EXCEPTION)
                    .addLog("log", e.getMessage());
        }
    }

    public Object jsonToObject(Class c, String json) {
        try {
            return mapper.readValue(json, c);
        } catch (IOException e) {
            log.error("Cannot deserialize JSON as instance of class {}", c.getName());
            throw ServiceException.wrap(e, ErrorCode.JSON_CONVERTER_EXCEPTION)
                    .addLog("json", json)
                    .addLog("target class", c.getName());
        }
    }

    public String messageErrorToJson(String message) {
        Map result = Collections.singletonMap("responseError", message);
        return objectToJson(result);
    }
}

That works in gradle bootRun, as well as in tests like this:

@Slf4j
@SpringBootTest // we can only launch the whole context to make Jackson2ObjectMapperBuilder to work
@WebAppConfiguration
@ContextConfiguration
class JsonConverterTest {

    @Autowired
    private JsonConverter jsonConverter;

    ...

I know it works, because when debugging it stops a breakpoint in JacksonAutoConfiguration class.

But, I wonder if there is another simpler way to get it. Why I have to use @WebAppConfiguration and @ContextConfiguration? Why doesn't @SpringBootTest load the application.yml spring.jackson.xx values?

Or, to make sure the application.yml is loaded, how to limit the classes to load in a SpringBootTest to minimize the number of classes to load, and speed up the tests?

EDIT: If I delete @WebAppConfiguration and ContextConfiguration, and add @SpringBootTest(class = MyApplication.class), it also loads all the context/launch the whole app and works.


Solution

  • I'm looking for the same thing. However, for now, I use @JsonTest on my unit test class to avoid full Spring initialization, and it works fine. I also found that the same effect can be achieved using combination @ExtendWith + @ContextConfiguration or @ExtendWith + @Import.

    My code is as follows:

    // This test class is under the same packages where @SpringBootApplication in src/main/java is written.
    package com.example;
    
    import com.example.security.user.profile.*;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import lombok.extern.log4j.Log4j2;
    
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
    import org.springframework.boot.test.autoconfigure.json.JsonTest;
    import org.springframework.context.annotation.Import;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
    
    // This annotation comes from spring-boot-autoconfigure, please check if
    // this dependency is already there, if you have spring-boot-starter-web
    // or spring-boot-starter-test it is already there.
    @JsonTest
    
    // Or use the following two combinations instead of @JsonTest:
    // @ExtendWith(SpringExtension.class)
    // @Import(JacksonAutoConfiguration.class)
    
    // Or use the following two combinations instead of @JsonTest:
    // @ExtendWith(SpringExtension.class)
    // @ContextConfiguration(classes = {JacksonAutoConfiguration.class})
    
    @Log4j2
    class MyUserProfileTestCase {
    
        @Test
        void serializationCase(@Autowired ObjectMapper objectMapper) {
            var profile = new UserProfile();
            profile.setEmail("ximinghui@example.com");
            profile.setFullName(new PersonName("Xi", "Minghui"));
            profile.setSex(Sex.MALE);
    
            assertDoesNotThrow(() -> log.info(objectMapper.writeValueAsString(profile)));
        }
    
    }
    

    I'm not sure about their principles either.