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