web-dev-qa-db-fra.com

Impossible de simuler la classe de service dans les tests du contrôleur Spring MVC

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.


Mise à jour 1

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.

41
stevenghines

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.

75
stevenghines

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;".

8
Dino Tw

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());
}
5
Swarit Agarwal

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;
3
kaskelotti

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.

2
Tom Verelst

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.

0
Krushna