web-dev-qa-db-ja.com

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"));
    }
}
_

2つのコンテキスト構成ファイルがあることに気付くでしょう。これは、実際のサービスレイヤーにヒットするコントローラーテストを停止できない場合、そのサービスレイヤーのリポジトリがテストデータベースを指している可能性があるため、ハックです。私はもうこのハックから逃れることができず、自分のサービス層を適切にモックアウトできる必要はありません。

なぜwhen(service.save(isA(Policy.class))).thenReturn(new Policy());がPolicyServiceのsaveメソッドを開始およびモックアウトしないのですか?どこかにmockitoの設定がありませんか? Spring構成に配置する必要があるものはありますか?私のこれまでの研究は、グーグルの「スプリングmvcテストmockitoが動作していない」に限定されていましたが、それでも先に進むことはあまりありませんでした。

ありがとう。


アップデート1

あなたは正しい@ tom-verelstでした。テストでは_PolicyService service;_行を参照していましたので、MockMvc内のサービスはもちろんSpringによって注入されます。

私は少し調査を行った結果、 ブログ投稿 が見つかりました。これは_@InjectMocks_の使用目的を説明するのに適していました。

その後、_private MockMvc mockMvc_に_@InjectMocks_の注釈を付けてみましたが、それでも同じ問題が発生しました(つまり、MockMvc内のサービスはnot私がそれを期待していたようにm笑した。デバッグ中に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   
_

より多くの研究( @ Mockを使用する場合、MockitoがSpring BeanにNull値を挿入しますか? )は、テスト内のPolicyControllerメンバー変数に_@InjectMocks_を適用することを提案しましたが、最初のリンクの回答の1つでは、Springはそれについて何も知らないため、これは何もしません。

41
stevenghines

@J Andyの思考のおかげで、私はこれについて間違った道を進んでいたことに気付きました。 Update 1では、モックサービスをMockMvcに挿入しようとしましたが、一歩下がった後、テスト中のMockMvcではなく、PolicyControllerであることがわかりました。テストする。

少し背景を説明するために、Spring MVCアプリケーションで@Controllersの従来の単体テストを回避したかったのは、Spring自体内でコントローラーを実行することによってのみ提供されるもの(たとえば、コントローラーアクションへのRESTful呼び出し)をテストしたいためです。これは、Spring内でテストを実行できる Spring MVC Test framework を使用して実現できます。

最初の質問のコードから、Spring MVCテストをWebApplicationContext(つまりthis.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();)で実行していたのに対し、私はshouldスタンドアロンで実行されていました。スタンドアロンで実行すると、テストするコントローラーを直接注入できるため、サービスをコントローラーに注入する方法を制御できます(つまり、モックサービスを強制的に使用できます)。

これはコードで簡単に説明できます。したがって、次のコントローラーの場合:

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に関しては今でも多くのことを学んでいるので、説明を改善するコメントを歓迎します。 このブログ投稿 は、このソリューションを思い付くのに役立ちました。

75
stevenghines

このセクション、Springドキュメントの11.3.6 Spring MVCテストフレームワーク 11。Testing で説明されていますが、いずれにせよ明確ではありません。

説明のためにドキュメントの例を続けましょう。サンプルテストクラスは次のようになります

@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のセッターインジェクションに変更する必要があります。また、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 org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;をインポートしてください」。

8
Dino Tw

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

これはおそらく、SpringとMockitoが両方ともBeanを注入しようとする場合の問題です。これらの問題を回避する方法の1つは、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 );
}

追伸あなたの命名規則は私見から少し外れており、mockMvcがあなたがテストしようとしているクラス(UUT)であると仮定しています。代わりに次の名前を使用します

@Mock PolicyService mockPolicyService;
@InjectMocks Mvc mvc;
3
kaskelotti

PolicyServiceのモックを作成していますが、私が知る限り、それをMockMvcに挿入していません。これは、Spring構成で定義されたPolicyServiceがモックの代わりに呼び出されることを意味します。

PolicyServiceのモックを設定してMockMvcにインジェクトするか、モックのインジェクションについて Springockito を見てください。

2
Tom Verelst

@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")));
  }

}

ここでは、SpringレストコントローラクラスであるCategoryAPIクラスのみをロードし、残りはすべてモックです。 Springには、@ MockBeanおよび@SpyBeanのような独自の注釈バージョンがあり、mockito @Mockおよび@Spyに似ています。

0
Krushna