58 votes

Impossible de simuler la classe 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 les requêtes GET et POST sur les actions de mes contrôleurs. J'utilise Mockito pour simuler les services, mais je constate que les simulations sont ignorées et que ma couche de service réelle est utilisée (et, par conséquent, la base de données est touchée).

Le code de 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 une astuce car si je ne peux pas empêcher le test du contrôleur de toucher la couche de service réelle, alors cette couche de service pourrait tout aussi bien avoir ses référentiels pointant vers la base de données de test. Je ne suis pas arrivé à un point où je ne peux plus m'en sortir avec ce hack et j'ai besoin d'être capable de simuler ma couche de service correctement.

Pourquoi la when(service.save(isA(Policy.class))).thenReturn(new Policy()); n'intervient pas et ne se moque pas de la méthode save dans le PolicyService ? Est-ce qu'il me manque une configuration de mockito quelque part ? Est-ce qu'il y a quelque chose que je dois mettre dans la configuration de Spring ? Pour l'instant, mes recherches se sont limitées à Googler "spring mvc test mockito not working" mais cela ne m'a pas donné beaucoup d'éléments pour avancer.

Merci.


Mise à jour 1

Vous aviez raison @tom-verelst, je faisais référence à la PolicyService service; dans mon test pour que le service à l'intérieur du MockMvc aura bien sûr été injecté par Spring.

J'ai fait quelques recherches et j'ai trouvé un article de blog qui a fait un bon travail d'explication de ce qu'est un @InjectMocks est utilisé pour.

J'ai ensuite essayé d'annoter private MockMvc mockMvc con @InjectMocks et j'ai toujours le même problème (c'est-à-dire que le service à l'intérieur du fichier MockMvc était no comme je m'y attendais). J'ai ajouté la trace de pile au moment du débogage où la méthode de sauvegarde sur le fichier PolicyServiceImpl est appelée (par opposition à l'appel souhaité à la méthode save 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 Injecter des valeurs Null dans un Spring bean lors de l'utilisation de @Mock ? ) a suggéré d'appliquer la @InjectMocks à un PolicyController dans le test, mais comme indiqué dans l'une des réponses du premier lien, cela ne fait rien parce que Spring ne sait rien à ce sujet.

97voto

stevenghines Points 1591

Grâce à la réflexion de @J Andy, j'ai réalisé que j'avais fait fausse route sur ce sujet. Dans la mise à jour 1, j'essayais d'injecter le service fictif dans le fichier MockMvc mais après avoir pris un peu de recul, je me suis rendu compte que ce n'est pas la MockMvc qui était testée, c'était la PolicyController J'ai voulu tester.

Pour donner un peu de contexte, je voulais éviter un test unitaire traditionnel des @Contrôleurs dans mon application Spring MVC parce que je voulais tester des choses qui ne sont fournies que par l'exécution des contrôleurs dans Spring lui-même (par exemple, les appels RESTful aux actions des contrôleurs). Ceci peut être réalisé en utilisant la méthode Cadre de test Spring MVC qui vous permet d'exécuter vos tests au sein de Spring.

Vous verrez dans le code de ma question initiale que j'exécutais les tests Spring MVC dans un fichier WebApplicationContext (c'est-à-dire this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); ) alors que ce que je devrait était de fonctionner en mode autonome. L'exécution en mode autonome me permet d'injecter directement le contrôleur que je veux tester et, par conséquent, de contrôler la manière dont le service est injecté dans le contrôleur (c'est-à-dire de forcer l'utilisation d'un service fictif).

Il est plus facile de l'expliquer dans le code. Ainsi, 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"));
    }
}

Je suis encore en phase d'apprentissage en ce qui concerne Spring, donc tout commentaire permettant d'améliorer mon explication sera le bienvenu. Cet article de blog m'a aidé à trouver cette solution.

14voto

Swarit Agarwal Points 592

Je préférerais un service autonome de Mockmvc

Mentionné travail 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());
}

8voto

Dino Tw Points 331

Cette section, 11.3.6 Spring MVC Test Framework, dans le document Spring 11. Essais en parle, mais il n'est pas clair d'une manière ou d'une autre.

Reprenons l'exemple du document pour l'expliquer. L'exemple de classe de test se présente comme 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 comme contrôleur. Dans le fichier test-servlet-context.xml, vous devrez avoir

<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>

Il manque dans le document la partie relative au câblage du contrôleur. Et vous devrez passer à l'injection de setter pour accountService si vous utilisez l'injection de champ. Notez également que la valeur (org.example.AccountService ici) de l'argument de construction est une interface, et non une classe.

Dans la méthode d'installation 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 d'essai peut se présenter comme suit

@Test
public void testAccountId(){
    this.mockMvc.perform(...)
    .andDo(print())
    .andExpect(...);  
}

andDo(print()) est pratique, faites "import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print ;".

6voto

Zeljko Kozina Points 89

Si vous êtes sur SpringBoot, utilisez Springs @MockBean . - comme le disent les docks :

Tout haricot unique existant du même type défini dans la section sera remplacé par le simulacre. Si aucun bean existant n'est défini, un nouveau bean sera sera ajouté.

 @RunWith(SpringRunner.class)
 public class ExampleTests {

     @MockBean
     private ExampleService service;

3voto

J Andy Points 670

Il s'agit probablement d'un problème lié au fait que Spring et Mockito tentent tous deux d'injecter les beans. Une façon d'éviter ces problèmes est d'utiliser Spring ReflectionTestUtils pour injecter manuellement le simulacre de service.

Dans ce cas, votre setup() ressemblerait à ce qui suit

@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 erronée IMHO 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;

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X