簡體   English   中英

無法在 Spring MVC 控制器測試中模擬服務類

[英]Unable to mock Service class in Spring MVC Controller tests

我有一個 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"));
    }
}

你會注意到我有兩個上下文配置文件; 這是一個 hack,因為如果我無法阻止控制器測試到達實際的服務層,那么該服務層可能會將其存儲庫指向測試數據庫。 我還沒有到無法再擺脫這種黑客攻擊並且需要能夠正確模擬我的服務層的地步。

為什么是when(service.save(isA(Policy.class))).thenReturn(new Policy()); 沒有在 PolicyService 中啟動和模擬 save 方法? 我在某處錯過了一些模擬配置嗎? 我需要在 Spring 配置中添加什么嗎? 到目前為止,我的研究僅限於谷歌搜索“spring mvc test mockito not working”,但這並沒有給我太多的繼續。

謝謝。


更新 1

你是對的@tom-verelst,我指的是PolicyService service; 我的測試中的行,所以MockMvc中的服務當然會被 Spring 注入。

我做了一些研究,發現一篇博客文章很好地解釋了@InjectMocks的用途。

然后我嘗試用@InjectMocks 注釋private MockMvc mockMvc @InjectMocks並且仍然遇到同樣的問題(即MockMvc內部的服務沒有像我預期的那樣被模擬)。 我在調試期間調用PolicyServiceImpl上的 save 方法時添加了堆棧跟蹤(與模擬服務中對 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 values into a Spring bean when using @Mock? )建議在測試@InjectMocks應用於PolicyController成員變量,但正如第一個鏈接中的一個答案所指出的那樣,這沒有任何作用,因為 Spring對此一無所知。

感謝@J Andy 的思路,我意識到我在這方面走錯了路。 在 Update 1 中,我試圖將模擬服務注入到MockMvc ,但退后一步后我意識到正在測試的不是MockMvc ,而是我想要測試的PolicyController

為了提供一些背景知識,我想避免在我的 Spring MVC 應用程序中對 @Controllers 進行傳統的單元測試,因為我想測試僅通過在 Spring 本身中運行控制器提供的東西(例如,對控制器操作的 RESTful 調用)。 這可以通過使用Spring MVC 測試框架來實現,該框架允許您在 Spring 中運行測試。

您將從我最初問題中的代碼中看到,我正在WebApplicationContext中運行 Spring MVC 測試(即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,我仍然在學習很多東西,因此歡迎任何可以改進我的解釋的評論。 這篇博文對我提出這個解決方案很有幫助。

我更喜歡 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());
}

這部分,11.3.6 Spring MVC 測試框架,在 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>

該文檔缺少控制器的接線部分。 如果您使用字段注入,則需要更改 accountService 的 setter 注入。 另外,請注意,constructor-arg 的值(此處為 org.example.AccountService)是一個接口,而不是一個類。

在 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(...);  
}

andDo(print()) 很方便,做“import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;”。

如果在 SpringBoot 上使用 Springs 自己的@MockBean - 正如碼頭所說:

在上下文中定義的任何現有的相同類型的單個 bean 都將被模擬替換。 如果沒有定義現有的 bean,將添加一個新的。

 @RunWith(SpringRunner.class)
 public class ExampleTests {

     @MockBean
     private ExampleService service;

這可能是 Spring 和 Mockito 都嘗試注入 bean 的問題。 我能想到的避免這些問題的一種方法是使用 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 );
}

PS 您的命名約定有點偏離恕我直言,我假設 mockMvc 是您要測試的類(UUT)。 我會改用以下名稱

@Mock PolicyService mockPolicyService;
@InjectMocks Mvc mvc;

使用@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 REST 控制器類,其余都是模擬的。 Spring 有自己的注解版本,例如 @MockBean 和 @SpyBean,類似於 mockito @Mock 和 @Spy。

您正在為PolicyService創建一個模擬,但據我所知,您並沒有將其注入到您的MockMvc中。 這意味着將調用 Spring 配置中定義的PolicyService而不是模擬。

通過設置將MockMvc PolicyService ,或者查看Springockito以注入模擬。

替代方案:

  1. 使用WebApplicationContext時,會在spring容器中查找依賴。 所以如果我們想模擬一些東西,我們應該在 spring 容器中做這個,那就是你的上下文配置文件。

    @Bean public FileResourceService fileResourceService() { FileResourceServiceImpl service = Mockito.mock(FileResourceServiceImpl.class); 退貨服務; }

  2. 當你在測試類中使用這個 Mock 注解時,它對 spring 容器沒有任何意義。 他們對此一無所知。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM