J'ai une application Spring 3.2 MVC et j'utilise le framework de test Spring MVC pour tester GET et POST demandes sur les actions de mes contrôleurs. J'utilise Mockito pour simuler les services, mais je constate que le les simulacres sont ignorés et que ma couche de service actuelle est utilisée (et, par conséquent, la base de données est touchée).
Le code dans mon test de contrôleur:
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"));
}
}
Vous remarquerez que j'ai deux fichiers de configuration de contexte; c'est un hack, car si je ne parviens pas à arrêter le test du contrôleur sur la couche de service réelle, celle-ci peut également avoir ses référentiels pointant vers la base de données de test. Je ne suis pas à un point où je ne peux plus m'en tirer et j'ai besoin de pouvoir simuler ma couche de service correctement.
Pourquoi la when(service.save(isA(Policy.class))).thenReturn(new Policy());
ne lance-t-elle pas et ne se moque-t-elle pas de la méthode de sauvegarde dans PolicyService? Est-ce que quelque chose me manque quelque part dans la configuration de Mockito? Dois-je ajouter quelque chose à la configuration Spring? Ma recherche jusqu’à présent s’est limitée à Googling "Le test du printemps mvito ne fonctionne pas", mais cela ne m’a pas donné grand-chose à faire.
Merci.
Vous aviez raison @ tom-verelst, je faisais référence à la ligne PolicyService service;
Dans mon test afin que le service à l'intérieur de MockMvc
ait bien sûr été injecté par Spring.
J'ai fait un peu de recherche et trouvé n article de blog qui expliquait très bien à quoi sert @InjectMocks
.
J'ai ensuite essayé d'annoter private MockMvc mockMvc
Avec @InjectMocks
Et j'ai toujours le même problème (c'est-à-dire que le service à l'intérieur du MockMvc
était pas s'est moqué comme je m'attendais à ce qu'il soit). J'ai ajouté la trace de pile au point pendant le débogage où la méthode de sauvegarde sur le PolicyServiceImpl
est appelée (par opposition à l'appel souhaité de la méthode de sauvegarde dans le service simulé).
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
Plus de recherche ( --- (Mockito Injecting Null dans un haricot Spring en utilisant @Mock? ) a suggéré d'appliquer le @InjectMocks
À une variable membre PolicyController
dans le test, mais comme indiqué dans l'une des réponses du premier lien, cela ne fait rien car Spring ne sait rien à ce sujet.
Grâce à la réflexion de @J Andy, j'ai compris que je m'étais engagé dans la mauvaise voie. Dans la mise à jour 1, j'essayais d'injecter le service factice dans le MockMvc
mais après avoir pris du recul, j'ai réalisé que ce n'était pas le MockMvc
qui était en cours de test, mais bien le PolicyController
Je voulais tester.
Pour donner un peu de contexte, je voulais éviter un test unitaire traditionnel des @Controllers dans mon application Spring MVC car je voulais tester des éléments fournis uniquement par l'exécution des contrôleurs au sein même de Spring (par exemple, appels RESTful à des actions de contrôleur). Ceci peut être réalisé en utilisant le cadre de test Spring MVC qui vous permet d'exécuter vos tests dans Spring.
Vous verrez dans le code de ma question initiale que j'exécutais les tests Spring MVC dans un WebApplicationContext
(c'est-à-dire this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
) alors que moi aurait dû faire était en cours d'exécution autonome. Le fonctionnement autonome me permet d’injecter directement le contrôleur que je souhaite tester et d’avoir par conséquent le contrôle sur la façon dont le service est injecté dans le contrôleur (c’est-à-dire forcer l’utilisation d’un service fictif).
Ceci est plus facile à expliquer dans le code. Donc pour le contrôleur suivant:
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";
}
}
J'ai maintenant la classe de test suivante dans laquelle le service est simulé avec succès et ma base de données de test n'est plus touchée:
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"));
}
}
J'apprends toujours beaucoup en ce qui concerne le printemps, alors tout commentaire susceptible d'améliorer mon explication serait le bienvenu. Ce blog m'a été utile pour proposer cette solution.
Cette section, 11.3.6 Spring MVC Test Framework, dans le document Spring 11. Testing en parle, mais ce n’est pas clair.
Continuons avec l'exemple du document pour l'explication. L'exemple de classe de test ressemble à ce qui suit
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Autowired
private AccountService accountService;
// ...
}
Supposons que vous ayez org.example.AppController en tant que contrôleur. Dans le fichier test-servlet-context.xml, vous aurez besoin de
<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>
La pièce de câblage pour le contrôleur manque dans le document. Et vous aurez besoin de changer l'injecteur pour accountService si vous utilisez l'injection sur le terrain. Notez également que la valeur (org.example.AccountService ici) de constructor-arg est une interface et non une classe.
Dans la méthode de configuration de AccountTests, vous aurez
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
// You may stub with return values here
when(accountService.findById(1)).thenReturn(...);
}
La méthode de test peut ressembler à
@Test
public void testAccountId(){
this.mockMvc.perform(...)
.andDo(print())
.andExpect(...);
}
andDo (print ()) est pratique, faites "importer statique org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;".
Je préférerais un service autonome de Mockmvc
Travail mentionné pour moi
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());
}
C'est probablement un problème avec Spring et Mockito qui tentent tous deux d'injecter les haricots. Pour éviter ces problèmes, une solution consiste à utiliser Spring ReflectionTestUtils pour injecter manuellement le modèle de service.
Dans ce cas, votre méthode setup () ressemblerait à quelque chose comme ceci
@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. Votre convention de nommage est un peu à mon humble avis et je suppose que mockMvc est la classe que vous essayez de tester (UUT). J'utiliserais plutôt les noms suivants
@Mock PolicyService mockPolicyService;
@InjectMocks Mvc mvc;
Vous créez une maquette pour PolicyService
, mais vous ne l'injectez pas dans votre MockMvc
pour autant que je sache. Cela signifie que le PolicyService
défini dans votre configuration Spring sera appelé à la place de votre maquette.
Soit injecter la maquette du PolicyService
dans votre MockMvc
en la configurant, soit jeter un oeil sur Springockito pour l'injection de simulacres.
Il existe une autre solution avec la dernière version du printemps utilisant @WebMvcTest. Exemple ci-dessous.
@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")));
}
}
Ici, nous chargeons uniquement la classe CategoryAPI, qui est une classe de contrôleurs de repos Spring, et tous sont simulés. Spring possède sa propre version d'annotation, telle que @MockBean et @SpyBean, de la même manière que mockito @Mock et @Spy.