Spring FrameworkのUnitテスト実装方法 3-3.Controllerテスト(Junit4, spring-test, MockMvc(WebAppContextSetupMode), MockClass)

Spring FrameworkのUnitテスト実装方法 3-3.Controllerテスト(Junit4, spring-test, MockMvc(WebAppContextSetupMode), MockClass)

Spring FrameworkのUnitテスト実装方法 3-3.Controllerテスト(Junit4, spring-test, MockMvc(WebAppContextSetupMode), MockClass)

【サンプルソース】
TERASOLUNA Server Framework for Java (5.x) Development Guideline
サンプルソースはこちら
(これのMyBatis3を使用したパターンで作成してます。)

3-3.Controllerテスト Junit4, spring-testライブラリ、MockClassによるモック化を使用したパターン(WebAppContextSetupMode)

controllerクラスのテストです。
Serviceクラスのメソッドをモッククラスを用いてモック化しています。
3-1でもモッククラスによるモック化を行いましたが、今回はMockMvcのWebAppContextSetupModeパターンを使ってモック化します。

【pom.xml】

Junit、spring-testライブラリが必要です。

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

【テスト対象クラス】

TodoController.java
テスト対象はfinishメソッド

@Controller
@RequestMapping("todo")
public class TodoController {

    @Inject
    TodoService todoService;
    
    @Inject
    Mapper beanMapper;
    
    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }
    
    @RequestMapping(value = "finish", method = RequestMethod.POST)
    public String finish(@Validated({Default.class}) TodoForm form, BindingResult bindingResult, Model model, RedirectAttributes attributes) {
        if(bindingResult.hasErrors()) {
            return list(model);
        }
        
        try {
            todoService.finish(form.getTodoId());
        }catch (BusinessException e) {
            // TODO: handle exception
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        
        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage.fromText("Finished successfully!")));
        return "redirect:/todo/list";
    }
}

(対象メソッドの処理の部分だけ抜粋してます。)

【テストクラス】

TodoControllerTestVerWebAppContext

@RunWith(SpringJUnit4ClassRunner.class)
//設定ファイルの読み込み
@ContextConfiguration({
    "classpath:META-INF/spring/test-context-webappcontextsetup.xml",
    "classpath:META-INF/spring/test-mvc.xml"})
//設定ファイルのオブジェクトと、ServletAPIに依存する各種モックオブジェクトのインジェクションを行う
@WebAppConfiguration
public class TodoControllerTestVerWebAppContext {
    //applicationContext.xml
    MockMvc mockMvc;
    
    @Inject
    WebApplicationContext wac;
    
    @Before
    public void setUp() throws Exception {
        //webAppContextSetupモードでmockMvcを起動
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    //正常動作のパターン
    @Test
    public void testFinish() throws Exception {
        
        //Controllerに投げるリクエストを作成
        MockHttpServletRequestBuilder getRequest = 
                MockMvcRequestBuilders.post("/todo/finish")
                                        .param("todoId", "cceae402-c5b1-440f-bae2-7bee19dc17fb")
                                        .param("todoTitle", "one");
        
        //Controllerにリクエストを投げる
        ResultActions results = mockMvc.perform(getRequest);
        
        //結果検証
        results.andDo(print());
        results.andExpect(status().isFound());
        results.andExpect(view().name("redirect:/todo/list"));
        
        //FlashMapのデータを取得(model.addFlashAttributeに設定したmessageオブジェクトのtextを取得する。半ば強引)
        FlashMap flashMap = results.andReturn().getFlashMap();
        Collection<Object> collection = flashMap.values();
        for(Object obj : collection) {
            ResultMessages messages = (ResultMessages) obj;
            ResultMessage message = messages.getList().get(0);
            String text = message.getText();
            assertEquals("Finished successfully!", text);
        }
    }
}

テストクラスは正常動作パターンだけです。
3-1でやったような異常系のパターンも入れたかったのですが、別のモッククラスを使用しているためこのクラスファイルでは入れませんでした。
3-1でのモッククラスの使用例ではテストメソッドの中で使用するモッククラスを指定して切り替えることができました。
一方で今回のテストクラスの場合、xmlの設定ファイルの中でモッククラスを宣言します。そして、テストクラスの一番最初に設定ファイルを読み込む宣言を行うため、テストメソッドごとのモッククラスの切り替えができません。よって、テストメソッドごとに使用するモッククラスを切り替えたい場合はテストクラスそのものを別にする必要があります。

【その他設定】

test-context-webappcontextsetup.xml

<!-- モック対象のクラスをexclude-filterでコンポーネントスキャン対象から除外する -->
<context:component-scan base-package="todo.domain">
    <context:exclude-filter type="assignable" expression="todo.domain.service.todo.TodoServiceImpl"/>
</context:component-scan>

<!-- モッククラスをモック対象クラスに見せかけて設定する -->
<bean id="TodoserviceImplMock" class="todo.app.todo.TestFinishMock" />

設定ファイルの中で、モック化するクラスをconponent-scan対象から除外し、代わりにモッククラスを読み込ませます。

TestFinishMock

public class TestFinishMock implements TodoService{
    
    //モック用の戻り値データを作成
    public Todo getTodoTrueData() throws Exception {
        
        Todo todo = new Todo();
        todo.setTodoId("cceae402-c5b1-440f-bae2-7bee19dc17fb");
        todo.setTodoTitle("one");
        todo.setFinished(true);
        SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String strDate = "2017-10-01 15:39:17.888";
        Date date1 = sdFormat.parse(strDate);
        todo.setCreatedAt(date1);
        
        return todo;
    }
}

読み込ませたモッククラスです。implementsでモック対象のクラスが実装していたインタフェースを指定しているため、DIコンテナ上で自動的にTodoServiceインタフェースとこのモッククラスが紐づけられます。

WebAppContextSetupModeでモック化を行った時は、一つのクラスファイル内ではモッククラスの使い分けができないため、例えば正常系エラー系で使用するモッククラスを変えたいといった場合クラスファイルごと分けなければいけないというところがちょっと面倒かもしれないです。(もしかしたらいい方法があるかもしれませんが・・・)
StandAloneSetupパターンにはない大きなメリットとしてはやっぱりDIコンテナを使用してServiceとRepositoryも使った結合的なテストが行えるということですね。
なので単体試験にはStandAloneSetup、結合試験にはWebAppContextSetupというように使えるのではないでしょうか。

サンプルソースはgithubで公開してます。
Spring Unit Test pattern13

Controller Testカテゴリの最新記事