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

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

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

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

controllerクラスのテストです。
Serviceクラスのメソッドをモッククラスを用いてモック化しています。
モック化については2-2のServiceテストでやったことと変わらないのですが、Springにおけるcontrollerクラスのテストの特徴として、MockMVCというspring-testライブラリの中の機能を使う点にあります。MockMVCはspring frameworkで作られたwebアプリケーションのDIコンテナの機能の部分を疑似的に再現してくれるspring-testの機能です。
viewである画面で入力情報を入力し、ボタンを押下した後にcontroller層に遷移する流れを再現してくれます。
簡単に言うとテスト対象のcontrollerクラスへ疑似的なリクエストを投げてくれますよ。ということです。

さらに、MockMVCには2パターンの起動方法があります。

standaloneSetupパターン

・MockMVCの機能を使いつつ、必要最小限の構成でテストができる。
・controllerクラスを引数として起動するので、mockitoライブラリの使用が可能

webAppContextSetupパターン

・デプロイ環境とほぼ同様の条件でのテストができる。(DIコンテナの設定とかxmlの設定全部する。)
・クラス単位の起動というわけではないので、mockitoライブラリの使用が不可。
・モック化する場合はテスト用のcontext.xmlを作成してxml上でモック化する必要がある。

今回はstandaloneSetupパターンでの実装をしていきます。

【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";
    }
}

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

【テストクラス】

TodoControllerTestVerStandaloneMockClass.java

public class TodoControllerTestVerStandaloneMockClass {

    TodoController target;
    
    MockMvc mockMvc;
    
    @Before
    public void setUp() {
        //モッククラスをテストメソッドによって切り替えるため処理なし
    }
    
    //正常動作のパターン
    @Test
    public void testFinish() throws Exception {
        
        //Serviceのモック化
        target = new TodoController();
        TodoService mockService = new TestFinishMock();
        target.todoService = mockService;
        
        //standaloneモードでmockMvcを起動
        mockMvc = MockMvcBuilders.standaloneSetup(target).build();
        
        //Controllerに投げるリクエストを作成
        MockHttpServletRequestBuilder getRequest = 
                MockMvcRequestBuilders.post("/todo/finish")
                                        .param("todoId", "cceae402-c5b1-440f-bae2-7bee19dc17fb")
                                        .param("todoTitle", "one");
        
        //Controllerにリクエストを投げる(リクエストからfinishメソッドを起動させる。)
        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);
        }
    }
    
    //serviceメソッドが異常を返した場合
    @Test
    public void testFinishThrowException() throws Exception {
        
        //Serviceのモック化
        target = new TodoController();
        TodoService mockService = new TestFinishExceptionMock();
        target.todoService = mockService;
        
        ///standaloneモードでmockMvcを起動
        mockMvc = MockMvcBuilders.standaloneSetup(target).build();
        
        //Controllerに投げるリクエストを作成
        MockHttpServletRequestBuilder getRequest = 
                MockMvcRequestBuilders.post("/todo/finish")
                                        .param("todoId", "cceae402-c5b1-440f-bae2-7bee19dc17fb")
                                        .param("todoTitle", "one");
        
        //Controllerにリクエストを投げる(リクエストからfinishメソッドを起動させる。)
        ResultActions results = mockMvc.perform(getRequest);
        
        //結果検証
        results.andDo(print());
        results.andExpect(status().isOk());
        results.andExpect(view().name("todo/list"));
        
        //model Confirmation
        ModelAndView mav = results.andReturn().getModelAndView();
        
        //結果検証
        ResultMessages actResultMessages = (ResultMessages)mav.getModel().get("resultMessages");
        ResultMessage actResultMessage = actResultMessages.getList().get(0);
        String text = actResultMessage.getText();
        assertEquals("[E004]The requested Todo is not found. (id=cceae402-c5b1-440f-bae2-7bee19dc17fb)", text);
    }
}

今回テスト対象として選んだメソッドがリダイレクトを返すパターンなので少し注意です。
なので、結果検証で期待する値もisOK()ではなくisFound()としてます。200じゃなくて302を期待してます。
あと、その後の検証ではattributes.addFlashAttributeでリダイレクト先に受け渡すデータの中身を検証しているのですが、
addされているResultMessagesというのがサンプルとして使用したterasolunaの機能になるので、ここのデータの中身の確認の方法はあまり参考にならないかもしれません。

テストケースは正常系と異常系で2パターン用意してみました。
正常系の方はリダイレクト遷移しFlashAttributeでデータを受け渡しているのでそのデータを持ってきており、
異常系の方はViewにそのまま遷移しModelAttributeでデータをviewに渡しているのでそのデータを取得してます。

【その他設定】

TestFinishMock.java
正常系用のモッククラスです。
(モック対象メソッドの処理の部分だけ抜粋してます。)

public class TestFinishMock implements TodoService{

    //mockを作成
    @Override
    public Todo finish(String todoId) {
        // TODO 自動生成されたメソッド・スタブ
        
        Todo todo = new Todo();
        try {
            //getTodoTrueDataメソッドでテスト用データを作成
            todo = getTodoTrueData();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        return todo;
    }
}

TestFinishExceptionMock.java
異常系用のモッククラスです。
(モック対象メソッドの処理の部分だけ抜粋してます。)

public class TestFinishExceptionMock implements TodoService{
    
    //mockを作成(exceptionを返す)
    @Override
    public Todo finish(String todoId) {
        // TODO 自動生成されたメソッド・スタブ
        ResultMessages messages = ResultMessages.error();
        messages.add(ResultMessage.fromText("[E004]The requested Todo is not found. (id=cceae402-c5b1-440f-bae2-7bee19dc17fb)"));
        
        throw new BusinessException(messages);
    }
}

単純にメソッド単位という意味でのUnit testであれば、standaloneSetupパターンで行うのが望ましいのではないかと思います。
しかもmockitoとか使えてテスト実装が楽だし。(xmlでモック化とかめんどくさいし・・・)

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

Controller Testカテゴリの最新記事