I am trying to write a unit test to a custom deserializer that is instantiated using a constructor with an @Autowired parameter and my entity marked with @JsonDeserialize. It works fine in my integration tests where a MockMvc brings up spring serverside.
However with tests where objectMapper.readValue(...) is being called, a new instance of deserializer using default constructor with no parameters is instantiated. Even though
@Bean
public MyDeserializer deserializer(ExternalObject externalObject)
instantiates wired version of deserializer, real call is still passed to empty constructor and context is not being filled up.
I tried manually instantiating of a deserializer instance and registering it in ObjectMapper, but it only works if I remove @JsonDeserialize from my entity class (and it breaks my integration tests even if I do the same in my @Configuration class.) - looks related to this: https://github.com/FasterXML/jackson-databind/issues/1300
I can still test the deserializer behavior calling deserializer.deserialize(...) directly, but this approach doesn't work for me in tests that are not Deserializer's unit tests...
UPD: working code below
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.github.tomakehurst.wiremock.common.Json;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import java.io.IOException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@JsonTest
@RunWith(SpringRunner.class)
public class JacksonInjectExample {
private static final String JSON = "{\"field1\":\"value1\", \"field2\":123}";
public static class ExternalObject {
@Override
public String toString() {
return "MyExternalObject";
}
}
@JsonDeserialize(using = MyDeserializer.class)
public static class MyEntity {
public String field1;
public String field2;
public String name;
public MyEntity(ExternalObject eo) {
name = eo.toString();
}
@Override
public String toString() {
return name;
}
}
@Component
public static class MyDeserializer extends JsonDeserializer<MyEntity> {
@Autowired
private ExternalObject external;
public MyDeserializer() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
}
public MyDeserializer(@JacksonInject final ExternalObject external) {
this.external = external;
}
@Override
public MyEntity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
return new MyEntity(external);
}
}
@Configuration
public static class TestConfiguration {
@Bean
public ExternalObject externalObject() {
return new ExternalObject();
}
@Bean
public MyDeserializer deserializer(ExternalObject externalObject) {
return new MyDeserializer(externalObject);
}
}
@Test
public void main() throws IOException {
HandlerInstantiator hi = mock(HandlerInstantiator.class);
MyDeserializer deserializer = new MyDeserializer();
deserializer.external = new ExternalObject();
doReturn(deserializer).when(hi).deserializerInstance(any(), any(), eq(MyDeserializer.class));
final ObjectMapper mapper = Json.getObjectMapper();
mapper.setHandlerInstantiator(hi);
final MyEntity entity = mapper.readValue(JSON, MyEntity.class);
Assert.assertEquals("MyExternalObject", entity.name);
}
}
Very interesting question, it made me wonder how autowiring into jackson deserializers actually works in a spring application. The jackson facility that is used seems to be the HandlerInstantiator
interface, which is configured by spring to the SpringHandlerInstantiator
implementation, which just looks up the class in the application context.
So in theory you could setup an ObjectMapper
in your unit test with your own (mocked) HandlerInstantiator
, returning a prepared instance from deserializerInstance()
. It seems to be fine to return null
for other methods or when the class parameter does not match, this will cause jackson to create the instance on its own.
However, I do not think this is a good way to unit test deserialization logic, as the ObjectMapper
setup is necessarily different from what is used during actual application execution. Using the JsonTest
annotation as suggested in Anton's answer would be a much better approach, as you are getting the same json configuration that would be used during runtime.