在 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
没有实现 equals
和 hashCode
所以当你在
中使用它时
when(restaurantService.findByNameIgnoreCaseContaining(filter))
.thenReturn(Arrays.asList(first, second));
Mockito 实际上不匹配参数与 mock,因此它不 return 您给它的数组。您应该实施 equals
和 hashCode
,或者更快但可能更容易出错,将 when(...)
定义替换为:
when(restaurantService.findByNameIgnoreCaseContaining(refEq(filter)))
.thenReturn(Arrays.asList(first, second));
Matchers.refEq
将使用反射比较值,而不依赖于 .equals
比较。
我是单元测试的新手。我正在尝试对 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
没有实现 equals
和 hashCode
所以当你在
when(restaurantService.findByNameIgnoreCaseContaining(filter))
.thenReturn(Arrays.asList(first, second));
Mockito 实际上不匹配参数与 mock,因此它不 return 您给它的数组。您应该实施 equals
和 hashCode
,或者更快但可能更容易出错,将 when(...)
定义替换为:
when(restaurantService.findByNameIgnoreCaseContaining(refEq(filter)))
.thenReturn(Arrays.asList(first, second));
Matchers.refEq
将使用反射比较值,而不依赖于 .equals
比较。