Как вставить PersistentEntityResourceAssembler в методы запроса пользовательского @RepositoryRestController в модульном тесте @WebMvcTest - PullRequest
0 голосов
/ 13 октября 2018

Обзор

Как правильно настроить PersistentEntityResourceAssembler в методы запросов моего пользовательского контроллера REST во время модульного теста @WebMvcTest?

Первый вопрос SO.Заранее извиняюсь, вероятно, просто упустил что-то глупое.

Пример кода доступен на GitHub .

Оливер Гирке, где вы?: P

Подробности

У меня есть собственный REST-контроллер, помеченный @RepositoryRestController.Некоторые из его методов запроса имеют PersistentEntityResourceAssembler, вставленный в качестве аргумента, чтобы я мог вернуть ресурсы HAL.

@RepositoryRestController
@RestController
@RequestMapping(produces = MediaTypes.HAL_JSON_VALUE )
public class ReportRestController {

    private final ReportRepository reportRepository;

    public ReportRestController(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @GetMapping(path = "/custom/reports/{id}")
    public ResponseEntity<?> customReportsEndpoint(@PathVariable("id") Long id,
                PersistentEntityResourceAssembler entityAssembler) {

        return ResponseEntity.ok(entityAssembler.toResource(reportRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("No report found with ID: " + id))));
    }
}

Если я выполняю интеграционный тест (@SpringBootTest), это работает нормально:

import static org.hamcrest.CoreMatchers.*;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomRestControllerIntegrationTest {

    private static final String REPORT_NAME = "Report 1";

    @MockBean
    private ReportRepository repository;

    @Autowired
    private MockMvc mvc;

    @Test
    public void thisTestPasses() throws Exception {
        given(repository.findById(any())).willReturn(Optional.of(new Report(1L, REPORT_NAME, new User(1L, "pbriggs"))));

        mvc.perform(get("/custom/reports/1"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", equalTo(REPORT_NAME)))
                .andExpect(jsonPath("$._links.enteredBy").exists())
                .andReturn().getResponse();
    }
}

Однако, если я запускаю модульный тест (@WebMvcTest), PersistentEntityResourceAssembler не можетбыть построенным из-за неправильной инъекции «сущностей».

Причина: org.springframework.beans.BeanInstantiationException: Не удалось создать экземпляр [org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler]: Constructorбросил исключение;вложенное исключение - java.lang.IllegalArgumentException: сущности помечены как @NonNull, но имеют значение null

Полная трассировка стека доступна в GitHub (достигнут предел символов)

Код:

import static org.hamcrest.Matchers.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@WebMvcTest
@EnableSpringDataWebSupport
public class CustomRestControllerUnitTest1 {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private ReportRepository repository;

    @Test
    public void thisTestFails() throws Exception {
        Report report = new Report(1L,"Report 1", new User(1L,"pbriggs"));

        given(repository.findById(1L)).willReturn(Optional.of(report));

        // Fails with:
        //
        // Caused by: org.springframework.beans.BeanInstantiationException:
        // Failed to instantiate [org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler]:
        // Constructor threw exception; nested exception is java.lang.IllegalArgumentException:
        // entities is marked @NonNull but is null
        MvcResult mvcResult = mvc.perform(get("/custom/reports/1").accept(MediaTypes.HAL_JSON_VALUE))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("Report 1")))
                .andReturn();
    }

}

Я предполагаю, что это потому, что RepositoryRestMvcAutoConfiguration (и, следовательно, RepositoryRestMvcConfiguration) никогда не загружается.Это правильное поведение для @WebMvcTest, так как он загружает только веб-компоненты (@Controller, @ControllerAdvice, @JsonComponent, Converter / GenericConverter, Filter, WebMvcConfigurer и HandlerMethodArgumentResolver).

Кроме того, я получаю переполнение стекапримерно половину времени выполнения модульных тестов в примере кода на GitHub .Что-то странное, так как оно очень недетерминированное ..

Итак, повторюсь, как я могу правильно ввести PersistentEntityResourceAssembler в методы запроса моего пользовательского контроллера REST во время @WebMvcTest модульного теста?

Требования

  1. Это должно быть выполнено в модульном тесте;Я не хочу запускать весь контекст приложения
  2. Возвращенная полезная нагрузка json должна быть документом в формате HAL (должен иметь раздел _links и т. Д.)

Исследования и связанные с этим вопросы

1) Добавить @EnableSpringDataWebSupport

Вопрос StackOverflow: Как настроить модульные тесты для RepositoryRestController в Spring?

Этот вопрос / ответ рекомендуетдобавив @EnableSpringDataWebSupport, но это заставляет мой контроллер никогда не создаваться.Может быть, потому что он использует @Configuration и модульный тест не выполняется в полном контейнере?Я не знаю.

Обновление 28.10.18. Приведенный выше вопрос также привел к созданию запроса документации в JIRA.

2) Измените PersistentEntityResourceAssembler наRepositoryEntityLinks

StackOverflow вопрос: Тестирование пользовательского RepositoryRestController, использующего PersistentEntityResourceAssembler

ответ на этот вопрос рекомендуется *1098*изменив PersistentEntityResourceAssembler на RepositoryEntityLinks и сгенерировав ссылки вручную.Мне нужно, чтобы эти ссылки автоматически генерировались, как это делается с PersistentEntityResourceAssembler

3) Добавить @BasePathAwareController в контроллер

StackOverflow вопрос: JUnit дляRestController с параметром PersistentEntityResourceAssembler

Один из ответов указал мне на DATAREST-657 , в котором предлагалось добавить @BasePathAwareController, но это ничего не изменило.

4) Mocking PersistentEntityResourceAssembler

StackOverflow вопрос: Rest Controllers vs spring-data-rest RepositoryRestResource

Этот ответ рекомендуется посмеяться над PersistentEntityResourceAssembler идругие вещи, но я не мог заставить его работать.Я получил бы следующее исключение:

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.mockito.internal.junit.DefaultStubbingLookupListener]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.mockito.internal.junit.DefaultStubbingLookupListener and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.data.rest.webmvc.PersistentEntityResource["persistentEntity"]->org.springframework.data.mapping.PersistentEntity$MockitoMock$2115290768["mockitoInterceptor"]->org.mockito.internal.creation.bytebuddy.MockMethodInterceptor["mockHandler"]->org.mockito.internal.handler.InvocationNotifierHandler["mockSettings"]->org.mockito.internal.creation.settings.CreationSettings["stubbingLookupListeners"]->java.util.ArrayList[0])

    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:71)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:166)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:165)
    at com.prestonb.edu.CustomRestControllerUnitTest2.thisTestFails(CustomRestControllerUnitTest2.java:78)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.mockito.internal.junit.JUnitRule$1.evaluateSafely(JUnitRule.java:52)
    at org.mockito.internal.junit.JUnitRule$1.evaluate(JUnitRule.java:43)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
 Caused by: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.mockito.internal.junit.DefaultStubbingLookupListener]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.mockito.internal.junit.DefaultStubbingLookupListener and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.data.rest.webmvc.PersistentEntityResource["persistentEntity"]->org.springframework.data.mapping.PersistentEntity$MockitoMock$2115290768["mockitoInterceptor"]->org.mockito.internal.creation.bytebuddy.MockMethodInterceptor["mockHandler"]->org.mockito.internal.handler.InvocationNotifierHandler["mockSettings"]->org.mockito.internal.creation.settings.CreationSettings["stubbingLookupListeners"]->java.util.ArrayList[0])
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:291)
    at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:272)
    at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:224)
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:891)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
    ... 34 more
 Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.mockito.internal.junit.DefaultStubbingLookupListener and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.data.rest.webmvc.PersistentEntityResource["persistentEntity"]->org.springframework.data.mapping.PersistentEntity$MockitoMock$2115290768["mockitoInterceptor"]->org.mockito.internal.creation.bytebuddy.MockMethodInterceptor["mockHandler"]->org.mockito.internal.handler.InvocationNotifierHandler["mockSettings"]->org.mockito.internal.creation.settings.CreationSettings["stubbingLookupListeners"]->java.util.ArrayList[0])
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
    at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191)
    at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:312)
    at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71)
    at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
    at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1396)
    at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:913)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:285)
    ... 45 more
 org.springframework.data.rest.webmvc.PersistentEntityResource["persistentEntity"]
 ->org.springframework.data.mapping.PersistentEntity$MockitoMock$1659608278["mockitoInterceptor"]
 ->org.mockito.internal.creation.bytebuddy.MockMethodInterceptor["mockHandler"]
 ->org.mockito.internal.handler.InvocationNotifierHandler["mockSettings"]
 ->org.mockito.internal.creation.settings.CreationSettings["stubbingLookupListeners"]
 ->java.util.ArrayList[0])

Код:

import static org.hamcrest.CoreMatchers.*;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

public class CustomRestControllerUnitTest2 {

    @Mock
    private PersistentEntityResourceAssembler assembler;

    @Mock
    private PersistentEntityResourceAssemblerArgumentResolver assemblerResolver;

    @Mock
    private PersistentEntity<Report, ?> entity;

    @InjectMocks
    private ReportRestController controller;

    private MockMvc mvc;

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Mock
    private ReportRepository repository;

    @Before
    public void setup() {
        this.mvc = MockMvcBuilders.standaloneSetup(controller)
                .setCustomArgumentResolvers(assemblerResolver)
                .build();
    }

    @Test
    public void thisTestFails() throws Exception {
        Report report = new Report(1L,"Report 1", new User(1L,"pbriggs"));

        given(repository.findById(1L)).willReturn(Optional.of(report));
        given(assemblerResolver.supportsParameter(any())).willReturn(true);
        given(assemblerResolver.resolveArgument(any(), any(), any(), any())).willReturn(assembler);
        given(assembler.toResource(report)).willReturn(PersistentEntityResource.build(report, entity).build());

        MvcResult mvcResult = mvc.perform(get("/custom/reports/1").accept(MediaTypes.HAL_JSON_VALUE))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("Report 1")))
                .andExpect(jsonPath("$.name").exists())
                .andExpect(jsonPath("$._links.enteredBy").exists())
                .andReturn();
    }
}

Итак, я добавил собственный преобразователь аргументов в MockMvc, который исправил эту проблему, но мой JSON больше не форматировалсяправильно как HAL:

@Test
public void thisTestFails() throws Exception {
    Report report = new Report(1L,"Report 1", new User(1L,"pbriggs"));

    given(repository.findById(1L)).willReturn(Optional.of(report));
    given(assemblerResolver.supportsParameter(any())).willReturn(true);
    given(assemblerResolver.resolveArgument(any(), any(), any(), any())).willReturn(assembler);
    given(assembler.toResource(report)).willReturn(PersistentEntityResource.build(report, entity).build());

    MvcResult mvcResult = mvc.perform(get("/custom/reports/1").accept(MediaTypes.HAL_JSON_VALUE))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name", is("Report 1")))
            .andExpect(jsonPath("$.name").exists())
            // Fails:
            //
            // Caused by: com.jayway.jsonpath.PathNotFoundException: Missing property in path $['_links']
            .andExpect(jsonPath("$._links.enteredBy").exists())
            .andReturn();

    /*
     * Expected (HAL document):
     *
     * {
     *   "name" : "Report 1",
     *   "_links" : {
     *     "self" : {
     *       "href" : "http://localhost/reports/1"
     *     },
     *     "report" : {
     *       "href" : "http://localhost/reports/1"
     *     },
     *     "enteredBy" : {
     *       "href" : "http://localhost/reports/1/enteredBy"
     *     }
     *   }
     * }
     *
     * Actual (Normal json):
     *
     * {
     *   "id": 1,
     *   "name": "Report 1",
     *   "enteredBy": {
     *     "id": 1,
     *     "username": "pbriggs"
     *   }
     *   // plus a bunch of mockito properties
     * }
     */
}
...