在 Spring 引导控制器中测试模型属性

Testing model attribute in Spring Boot Controllers

我是单元测试的新手。我正在尝试对 Spring 启动应用程序控制器进行测试。但是测试找不到我的模型属性或类似的东西。您可以在下面找到我的代码,并希望能帮助我发现我做错了什么。提前致谢!

失败堆栈跟踪:

> java.lang.AssertionError: Model attribute 'restaurants'
Expected: a collection with size <2>
     but: collection size was <0>
    at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
    at org.springframework.test.web.servlet.result.ModelResultMatchers.match(ModelResultMatchers.java:58)
    at org.springframework.test.web.servlet.MockMvc.andExpect(MockMvc.java:171)
    at com.matmr.restaurantpoll.controller.RestaurantControllerTest.should_search(RestaurantControllerTest.java:79)
    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:497)
    at org.junit.runners.model.FrameworkMethod.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.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=11=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

RestaurantControllerTest.class

package com.matmr.restaurantpoll.controller;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
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 com.matmr.restaurantpoll.model.Category;
import com.matmr.restaurantpoll.model.Restaurant;
import com.matmr.restaurantpoll.model.filter.RestaurantFilter;
import com.matmr.restaurantpoll.service.RestaurantService;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@WebAppConfiguration
public class RestaurantControllerTest {

    @Mock
    private RestaurantService restaurantService;

    @InjectMocks
    private RestaurantController restaurantController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders.standaloneSetup(restaurantController).setRemoveSemicolonContent(false).build();

    }

    @Test
    public void should_search() throws Exception {

        RestaurantFilter filter = new RestaurantFilter();
        filter.setName(null);

        Restaurant first = new RestaurantBuilder()
                .id(1L)
                .name("Abra")
                .description("lots of food")
                .category(Category.ITALIAN).build();

        Restaurant second = new RestaurantBuilder()
                .id(2L)
                .name("Kadabra")
                .description("food for days")
                .category(Category.PIZZA).build();

        when(restaurantService.findByNameIgnoreCaseContaining(filter)).thenReturn(Arrays.asList(first, second));

        this.mockMvc.perform(get("/restaurants"))
            .andExpect(status().isOk())
            .andExpect(view().name("restaurantList"))
            .andExpect(model().attribute("restaurants", hasSize(2)))
            .andExpect(model().attribute("restaurants",
                    hasItem(allOf(
                            hasProperty("id", is(1L)),
                            hasProperty("name", is("Abra")),
                            hasProperty("description", is("lots of food"))
                            ))))
            .andExpect(model().attribute("restaurants",
                    hasItem(allOf(
                            hasProperty("id", is(2L)),
                            hasProperty("name", is("Kadabra")),
                            hasProperty("description", is("food for days"))
                            ))));

        verify(restaurantService, times(1)).findByNameIgnoreCaseContaining(filter);
        verifyNoMoreInteractions(restaurantService);

    }

}

RestaurantController.class

package com.matmr.restaurantpoll.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.matmr.restaurantpoll.model.Restaurant;
import com.matmr.restaurantpoll.model.filter.RestaurantFilter;
import com.matmr.restaurantpoll.service.RestaurantService;

@Controller
@RequestMapping("/restaurants")
public class RestaurantController {

    @Autowired
    private RestaurantService restaurantService;

    @RequestMapping
    public ModelAndView pesquisar(@ModelAttribute("filtro") RestaurantFilter filter) {

        List<Restaurant> filterRestaurants = restaurantService.findByNameIgnoreCaseContaining(filter);
        ModelAndView mv = new ModelAndView("restaurantList");
        mv.addObject("restaurants", filterRestaurants);

        return mv;
    }

}

RestaurantList.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
<head>
<title>Pesquisa de Restaurantes</title>
</head>

<section layout:fragment="conteudo">

    <div layout:include="MensagemGeral"></div>

    <div class="panel panel-default">
        <div class="panel-heading">
            <div class="clearfix">
                <h1 class="panel-title liberty-title-panel">Pesquisa de
                    Restaurantes</h1>
                <a class="btn btn-link liberty-link-panel"
                    th:href="@{/titulos/novo}">Cadastrar Novo Restaurante</a>
            </div>
        </div>

        <div class="panel-body">



            <div class="table-responsive">
                <table class="table table-bordered table-striped">
                    <thead>
                        <tr>
                            <th class="text-center col-md-1">#</th>
                            <th class="text-left col-md-2">Nome</th>
                            <th class="text-left col-md-3">Descrição</th>
                            <th class="text-left col-md-2">Categoria</th>
                            <th class="col-md-1"></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="restaurant : ${restaurants}">

                            <td class="text-center" th:text="${restaurant.id}"></td>

                            <td class="text-center" th:text="${restaurant.name}"></td>

                            <td th:text="${restaurant.description}"></td>

                            <td th:text="${restaurant.category.description}"></td>

                            <td class="text-center"><a class="btn btn-link btn-xs"
                                th:href="@{/restaurants/{id}(id=${restaurant.id})}"
                                title="Editar" rel="tooltip" data-placement="top"> <span
                                    class="glyphicon glyphicon-pencil"></span>
                            </a> <a class="btn btn-link btn-xs" data-toggle="modal"
                                data-target="#confirmRemove"
                                th:attr="data-id=${restaurant.id}, data-name=${restaurant.name}"
                                title="Excluir" rel="tooltip" data-placement="top"> <span
                                    class="glyphicon glyphicon-remove"></span>
                            </a></td>
                        </tr>
                        <tr>
                            <td colspan="6" th:if="${#lists.isEmpty(restaurants)}">Nenhum
                                restaurante foi encontrado!</td>
                        </tr>
                    </tbody>

                </table>

            </div>
        </div>

        <div layout:include="confirmRemove"></div>

    </div>
</section>
</html>

首先,您应该从测试中删除注释 class,它们用于集成测试,只会减慢您的测试速度。

就你的问题而言,我的猜测是你的 RestaurantFilter 没有实现 equalshashCode 所以当你在

中使用它时
when(restaurantService.findByNameIgnoreCaseContaining(filter))
    .thenReturn(Arrays.asList(first, second));

Mockito 实际上不匹配参数与 mock,因此它不 return 您给它的数组。您应该实施 equalshashCode,或者更快但可能更容易出错,将 when(...) 定义替换为:

when(restaurantService.findByNameIgnoreCaseContaining(refEq(filter)))
    .thenReturn(Arrays.asList(first, second));

Matchers.refEq 将使用反射比较值,而不依赖于 .equals 比较。