Не удалось высмеять класс обслуживания в тестах Spring MVC Controller
У меня есть приложение Spring 3.2 MVC, и я использую тестовую среду Spring MVC для тестирования запросов GET и POST на действия моих контроллеров. Я использую Mockito для издевательства над Сервисами, но обнаруживаю, что имитации игнорируются и что используется мой фактический Сервисный уровень (и, как следствие, происходит сбой базы данных).
Код в моем тесте контроллера:
package name.hines.steven.medical_claims_tracker.controllers;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import name.hines.steven.medical_claims_tracker.domain.Policy;
import name.hines.steven.medical_claims_tracker.services.PolicyService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
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 org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "classpath:/applicationContext.xml", "classpath:/tests_persistence-applicationContext.xml" })
public class PolicyControllerTest {
@Mock
PolicyService service;
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
// this must be called for the @Mock annotations above to be processed.
MockitoAnnotations.initMocks(this);
}
@Test
public void createOrUpdateFailsWhenInvalidDataPostedAndSendsUserBackToForm() throws Exception {
// Post no parameters in this request to force errors
mockMvc.perform(post("/policies/persist")).andExpect(status().isOk())
.andExpect(model().attributeHasErrors("policy"))
.andExpect(view().name("createOrUpdatePolicy"));
}
@Test
public void createOrUpdateSuccessful() throws Exception {
// Mock the service method to force a known response
when(service.save(isA(Policy.class))).thenReturn(new Policy());
mockMvc.perform(
post("/policies/persist").param("companyName", "Company Name")
.param("name", "Name").param("effectiveDate", "2001-01-01"))
.andExpect(status().isMovedTemporarily()).andExpect(model().hasNoErrors())
.andExpect(redirectedUrl("list"));
}
}
Вы заметите, что у меня есть два файла конфигурации контекста; это хак, потому что, если я не могу остановить тест контроллера, попадающий на реальный уровень обслуживания, тогда этот уровень обслуживания может также иметь свои репозитории, указывающие на базу данных теста. Я не нахожусь в такой ситуации, когда я больше не могу сойти с рук с этим хаком, и мне нужно правильно смоделировать мой уровень обслуживания.
Почему when(service.save(isA(Policy.class))).thenReturn(new Policy());
не включает и не отключает метод сохранения в PolicyService? Я где-то пропускаю конфигурацию mockito? Есть ли что-то, что мне нужно поставить в конфигурации Spring? Мои исследования до сих пор ограничивались поиском Google "весенний тест MVC Mockito не работает", но это не дало мне много времени для продолжения.
Благодаря.
Обновление 1
Вы были правы @tom-verelst, я ссылался на строку PolicyService service;
в моем тесте, поэтому служба внутри MockMvc
, конечно же, была введена Spring.
Я провел небольшое исследование и нашел сообщение в блоге, в котором хорошо объяснил, для чего используется @InjectMocks
.
Затем я попытался аннотировать private MockMvc mockMvc
с помощью @InjectMocks
и все еще получал ту же проблему (то есть служба внутри MockMvc
не была осмеяна, как я ожидал). Я добавил трассировку стека в тот момент во время отладки, когда вызывается метод save в PolicyServiceImpl
(в отличие от желаемого вызова метода save в проверяемой службе).
Thread [main] (Suspended (breakpoint at line 29 in DomainEntityServiceImpl) PolicyServiceImpl(DomainEntityServiceImpl<T>).save(T) line: 29
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25
Method.invoke(Object, Object...) line: 597
AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[]) line: 317
ReflectiveMethodInvocation.invokeJoinpoint() line: 183
ReflectiveMethodInvocation.proceed() line: 150
TransactionInterceptor$1.proceedWithInvocation() line: 96
TransactionInterceptor(TransactionAspectSupport).invokeWithinTransaction(Method, Class, TransactionAspectSupport$InvocationCallback) line: 260
TransactionInterceptor.invoke(MethodInvocation) line: 94
ReflectiveMethodInvocation.proceed() line: 172
JdkDynamicAopProxy.invoke(Object, Method, Object[]) line: 204
$Proxy44.save(DomainEntity) line: not available
PolicyController.createOrUpdate(Policy, BindingResult) line: 64
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25
Method.invoke(Object, Object...) line: 597
ServletInvocableHandlerMethod(InvocableHandlerMethod).invoke(Object...) line: 219
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 132
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 104
RequestMappingHandlerAdapter.invokeHandleMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 746
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 687
RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 80
TestDispatcherServlet(DispatcherServlet).doDispatch(HttpServletRequest, HttpServletResponse) line: 925
TestDispatcherServlet(DispatcherServlet).doService(HttpServletRequest, HttpServletResponse) line: 856
TestDispatcherServlet(FrameworkServlet).processRequest(HttpServletRequest, HttpServletResponse) line: 915
TestDispatcherServlet(FrameworkServlet).doPost(HttpServletRequest, HttpServletResponse) line: 822
TestDispatcherServlet(HttpServlet).service(HttpServletRequest, HttpServletResponse) line: 727
TestDispatcherServlet(FrameworkServlet).service(HttpServletRequest, HttpServletResponse) line: 796
TestDispatcherServlet.service(HttpServletRequest, HttpServletResponse) line: 66
TestDispatcherServlet(HttpServlet).service(ServletRequest, ServletResponse) line: 820
MockFilterChain$ServletFilterProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 168
MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 136
MockMvc.perform(RequestBuilder) line: 134
PolicyControllerTest.createOrUpdateSuccessful() line: 67
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25
Method.invoke(Object, Object...) line: 597
FrameworkMethod$1.runReflectiveCall() line: 44
FrameworkMethod$1(ReflectiveCallable).run() line: 15
FrameworkMethod.invokeExplosively(Object, Object...) line: 41
InvokeMethod.evaluate() line: 20
RunBefores.evaluate() line: 28
RunBeforeTestMethodCallbacks.evaluate() line: 74
RunAfterTestMethodCallbacks.evaluate() line: 83
SpringRepeat.evaluate() line: 72
SpringJUnit4ClassRunner.runChild(FrameworkMethod, RunNotifier) line: 231
SpringJUnit4ClassRunner.runChild(Object, RunNotifier) line: 88
ParentRunner$3.run() line: 193
ParentRunner$1.schedule(Runnable) line: 52
SpringJUnit4ClassRunner(ParentRunner<T>).runChildren(RunNotifier) line: 191
ParentRunner<T>.access$000(ParentRunner, RunNotifier) line: 42
ParentRunner$2.evaluate() line: 184
RunBeforeTestClassCallbacks.evaluate() line: 61
RunAfterTestClassCallbacks.evaluate() line: 71
SpringJUnit4ClassRunner(ParentRunner<T>).run(RunNotifier) line: 236
SpringJUnit4ClassRunner.run(RunNotifier) line: 174
JUnit4TestMethodReference(JUnit4TestReference).run(TestExecution) line: 50
TestExecution.run(ITestReference[]) line: 38
RemoteTestRunner.runTests(String[], String, TestExecution) line: 467
RemoteTestRunner.runTests(TestExecution) line: 683
RemoteTestRunner.run() line: 390
RemoteTestRunner.main(String[]) line: 197
В дополнительном исследовании (Mockito Injecting Null-значения в bean-компонент Spring при использовании @Mock?) было предложено применить @InjectMocks
к переменной-члену PolicyController
в тесте, но, как указано в одном из ответы в первой ссылке, это ничего не делает, потому что Spring ничего не знает об этом.
Ответы
Ответ 1
Благодаря линии мышления @J Энди я понял, что я пошел по неверному пути. В обновлении 1 я пытался внедрить moker-сервис в MockMvc
, но после того, как я сделал шаг назад, я понял, что это не тег MockMvc
, который тестировался, это был PolicyController
, который я хотел проверить.
Чтобы дать немного фона, я хотел избежать традиционного unit test @Controllers в моем приложении Spring MVC, потому что я хотел проверить вещи, которые предоставляются только при запуске контроллеров внутри самого Spring ( например, RESTful вызывает действия контроллера). Это может быть достигнуто с помощью Spring MVC Test framework, который позволяет запускать тесты в Spring.
В моем первоначальном вопросе вы увидите, что я запускал тесты Spring MVC в WebApplicationContext
(т.е. this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
), тогда как то, что я должен был делать, было автономным. Запуск автономного режима позволяет мне напрямую вводить контроллер, который я хочу проверить, и, следовательно, контролировать, как служба вводится в контроллер (т.е. Принудительно использовать макет службы).
Это проще объяснить в коде. Итак, для следующего контроллера:
import javax.validation.Valid;
import name.hines.steven.medical_claims_tracker.domain.Benefit;
import name.hines.steven.medical_claims_tracker.domain.Policy;
import name.hines.steven.medical_claims_tracker.services.DomainEntityService;
import name.hines.steven.medical_claims_tracker.services.PolicyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/policies")
public class PolicyController extends DomainEntityController<Policy> {
@Autowired
private PolicyService service;
@RequestMapping(value = "persist", method = RequestMethod.POST)
public String createOrUpdate(@Valid @ModelAttribute("policy") Policy policy, BindingResult result) {
if (result.hasErrors()) {
return "createOrUpdatePolicyForm";
}
service.save(policy);
return "redirect:list";
}
}
Теперь у меня есть следующий тестовый класс, в котором служба успешно издевается и моя тестовая база данных больше не удаляется:
package name.hines.steven.medical_claims_tracker.controllers;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import name.hines.steven.medical_claims_tracker.domain.Policy;
import name.hines.steven.medical_claims_tracker.services.PolicyService;
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.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/applicationContext.xml" })
public class PolicyControllerTest {
@Mock
PolicyService policyService;
@InjectMocks
PolicyController controllerUnderTest;
private MockMvc mockMvc;
@Before
public void setup() {
// this must be called for the @Mock annotations above to be processed
// and for the mock service to be injected into the controller under
// test.
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controllerUnderTest).build();
}
@Test
public void createOrUpdateFailsWhenInvalidDataPostedAndSendsUserBackToForm() throws Exception {
// POST no data to the form (i.e. an invalid POST)
mockMvc.perform(post("/policies/persist")).andExpect(status().isOk())
.andExpect(model().attributeHasErrors("policy"))
.andExpect(view().name("createOrUpdatePolicy"));
}
@Test
public void createOrUpdateSuccessful() throws Exception {
when(policyService.save(isA(Policy.class))).thenReturn(new Policy());
mockMvc.perform(
post("/policies/persist").param("companyName", "Company Name")
.param("name", "Name").param("effectiveDate", "2001-01-01"))
.andExpect(status().isMovedTemporarily()).andExpect(model().hasNoErrors())
.andExpect(redirectedUrl("list"));
}
}
Я все еще очень много разбираюсь, когда дело доходит до Spring, поэтому любые комментарии, которые улучшат мои объяснения, будут приветствоваться. Этот пост в блоге был полезен для меня в решении этого решения.
Ответ 2
Этот раздел, 11.3.6 Spring MVC Test Framework, в Spring документе 11. Тестирование говорит об этом, но это не понятно.
Продолжим пример в документе для объяснения. Класс тестирования образцов выглядит следующим образом
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Autowired
private AccountService accountService;
// ...
}
Предположим, что в качестве контроллера у вас есть org.example.AppController. В test-servlet-context.xml вам нужно будет
<bean class="org.example.AppController">
<property name="accountService" ref="accountService" />
</bean>
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>
В документе отсутствует проводная часть контроллера. И вам понадобится внести изменения в инсталляцию setter для accountService, если вы используете полевую инъекцию. Также следует отметить, что значение (org.example.AccountService здесь) для constructor-arg является интерфейсом, а не классом.
В методе настройки в AccountTests у вас будет
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
// You may stub with return values here
when(accountService.findById(1)).thenReturn(...);
}
Метод тестирования может выглядеть как
@Test
public void testAccountId(){
this.mockMvc.perform(...)
.andDo(print())
.andExpect(...);
}
иDo (print()) удобно, сделайте "import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;".
Ответ 3
Я бы предпочел автономную службу Mockmvc
Упомянутая работа для меня
public class AccessControllerTest {
private MockMvc mockMvc;
@Mock
private AccessControlService accessControlService;
@InjectMocks
private AccessController accessController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(accessController).build();
}
@Test
public void validAccessControlRequest() throws Exception {
Bundle bundle = new Bundle();
bundle.setAuthorized(false);
Mockito.when(accessControlService.retrievePatient(any(String.class)))
.thenReturn(bundle);
mockMvc.perform(get("/access/user?user=3")).andExpect(status().isOk());
}
Ответ 4
Вероятно, это проблема с Spring и Mockito, пытающимися вставить beans. Один из способов, я могу думать об избежании этих проблем, - это использовать Spring ReflectionTestUtils, чтобы вручную ввести служебный макет.
В этом случае ваш метод setup() будет выглядеть примерно так:
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
// this must be called for the @Mock annotations above to be processed.
MockitoAnnotations.initMocks(this);
// TODO: Make sure to set the field name in UUT correctly
ReflectionTestUtils.setField( mockMvc, "service", service );
}
P.S. Ваше соглашение об именах бит выключено IMHO, и я предполагаю, что mockMvc - это класс, который вы пытаетесь протестировать (UUT). Вместо этого я использовал бы следующие имена
@Mock PolicyService mockPolicyService;
@InjectMocks Mvc mvc;
Ответ 5
Вы создаете макет для PolicyService
, но вы не вводите его в свой MockMvc
, насколько я могу судить. Это означает, что PolicyService
, определенный в вашей конфигурации Spring, будет вызываться вместо вашего макета.
Либо введите mock PolicyService
в свой MockMvc
, установив его, либо взгляните на Springockito для инъекций издевается.
Ответ 6
Есть еще одно решение с последним весенним выпуском, использующее @WebMvcTest. Пример ниже.
@RunWith(SpringRunner.class)
@WebMvcTest(CategoryAPI.class)
public class CategoryAPITest {
@Autowired
private MockMvc mvc;
@MockBean
CategoryAPIService categoryAPIService;
@SpyBean
Utility utility;
PcmResponseBean responseBean;
@Before
public void before() {
PcmResponseBean responseBean = new PcmResponseBean("123", "200", null, null);
BDDMockito.given(categoryAPIService.saveCategory(anyString())).willReturn(responseBean);
}
@Test
public void saveCategoryTest() throws Exception {
String category = "{}";
mvc.perform(post("/api/category/").content(category).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(jsonPath("messageId", Matchers.is("123")))
.andExpect(jsonPath("status", Matchers.is("200")));
}
}
Здесь мы загружаем только класс CategoryAPI, который является классом контроллера отдыха Spring, и остальные все являются ложными. Spring имеет собственную версию аннотации, такую как @MockBean и @SpyBean, похожую на mockito @Mock и @Spy.