Тестирование пользовательского JsonDeserializer в Джексоне / SpringBoot - PullRequest
2 голосов
/ 04 февраля 2020

Я пытаюсь написать модульный тест для пользовательского десериализатора, который создается с использованием конструктора с параметром @Autowired, а моя сущность помечена @JsonDeserialize. Он отлично работает в моих интеграционных тестах, где Mock Mvc поднимает пружинный сервер.

Однако в тестах, где вызывается objectMapper.readValue (...), создается новый экземпляр десериализатора с использованием конструктора по умолчанию без параметров. Хотя

@Bean
public MyDeserializer deserializer(ExternalObject externalObject) 

создает экземпляр проводной версии десериализатора, реальный вызов все еще передается пустому конструктору, а контекст не заполняется.

Я попытался вручную создать экземпляр десериализатора и зарегистрировать его. в ObjectMapper, но он работает только в том случае, если я удаляю @JsonDeserialize из моего класса сущности (и это нарушает мои интеграционные тесты, даже если я делаю то же самое в своем классе @Configuration.) - выглядит так: https://github.com/FasterXML/jackson-databind/issues/1300

Я все еще могу проверить поведение десериализатора, вызывая deserializer.deserialize (...) напрямую, но этот подход не работает для меня в тестах, которые не являются модульными тестами Десериализатора ...

UPD: рабочий код ниже

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);
    }
}

Ответы [ 3 ]

4 голосов
/ 07 февраля 2020

Я не знаю, как установить это, в частности, с помощью инъекции Джексона, но вы можете проверить это с помощью пружинных Json тестов. Я думаю, что этот метод ближе к реальному сценарию и намного проще. Spring будет загружать только связанные с сериализацией / десериализацией bean-компоненты, поэтому вместо них вы должны предоставлять только пользовательские bean-компоненты или mocks.

@JsonTest
public class JacksonInjectExample {

  private static final String JSON = "{\"field1\":\"value1\", \"field2\":123}";

  @Autowired
  private JacksonTester<MyEntity> jacksonTester;

  @Configuration
  public static class TestConfiguration {
      @Bean
      public ExternalObject externalObject() {
          return new ExternalObject();
      }
  }

  @Test
  public void test() throws IOException {
      MyEntity result = jacksonTester.parseObject(JSON);
      assertThat(result.getName()).isEqualTo("MyExternalObject");
  }

Если вы хотите использовать mocks, используйте следующий фрагмент:

  @MockBean
  private ExternalObject externalObject;

  @Test
  public void test() throws IOException {
      when(externalObject.toString()).thenReturn("Any string");
      MyEntity result = jacksonTester.parseObject(JSON);
      assertThat(result.getName()).isEqualTo("Any string");
  }
1 голос
/ 09 февраля 2020

Очень интересный вопрос, он заставил меня задуматься о том, как на самом деле работает автоматическое подключение в десериализаторы Джексона в весеннем приложении. По-видимому, используемым средством Джексона является HandlerInstantiator интерфейс , который сконфигурирован пружиной для SpringHandlerInstantiator реализации , которая просто ищет класс в контексте приложения.

Таким образом, теоретически вы можете установить ObjectMapper в своем модульном тесте с вашим собственным (издевательским) HandlerInstantiator, возвращая подготовленный экземпляр из deserializerInstance(). Кажется, что было бы хорошо возвращать null для других методов или когда параметр класса не совпадает, это заставит Джексона создать экземпляр самостоятельно.

Однако я не думаю, что это хорошо способ модульного тестирования десериализации logi c, так как настройка ObjectMapper обязательно отличается от того, что используется во время фактического выполнения приложения. Использование аннотации JsonTest, предложенной в ответе Антона , было бы намного лучше, поскольку вы получаете ту же конфигурацию json, которая будет использоваться во время выполнения.

1 голос
/ 07 февраля 2020

Модульные тесты не должны зависеть от других основных классов или сред или вызывать их. Это особенно верно, если существуют также интеграционные или приемочные тесты, охватывающие функционирование приложения с определенным набором зависимостей, как вы описываете. Поэтому было бы лучше написать модульный тест так, чтобы в качестве субъекта у него был один класс, т.е. непосредственный вызов deserializer.deserialize (...).

В этом случае модульный тест должен состоять из создания экземпляра MyDeserializer. с поддельным или заглушенным ExternalObject, затем проверяет, что его метод deserialize () правильно возвращает MyEntity для различных состояний аргументов JsonParser и DeserializationContext. Mockito действительно хорош для установки фиктивных зависимостей!

При использовании ObjectMapper в модульном тесте довольно много кода из инфраструктуры Джексона также вызывается при каждом запуске - поэтому Тест не проверяет контракт MyDeserializer, он проверяет поведение комбинации MyDeserializer и определенного выпуска Джексона. Если тест не пройден, сразу не будет понятно, какой из компонентов задействован. А поскольку совместную настройку среды двух платформ сложнее, тест со временем станет хрупким и чаще будет проваливаться из-за проблем с настройкой в ​​классе теста.

Платформа Jackson отвечает за написание модуля тесты ObjectMapper.readValue и конструкторов с использованием @JacksonInject. Для «других модульных тестов, которые не являются модульными тестами Deserializer» - было бы лучше смоделировать / заглушить MyDeserializer (или другие зависимости) для этого теста. Таким образом, логика c другого класса изолируется от логики c в MyDeserializer - и контракты другого класса могут быть проверены без учета поведения кода вне тестируемого модуля.

...