Mockitoノススメ テストスタイルの変化
Mockitoを使うようになってから、僕はテストコードへの取り組みが変わりました。Mockitoを使うまで僕がUnit Testと思っていたものは、厳密にはUnit Testじゃないんじゃないか、と思うようになりました。なぜかというと、実装コードを書いていくと、たくさんのクラスと関連していきます。だんだんと、そのクラス、Unitをテストするのではなく、そのAPIの裏にあるクラスの状態、振る舞いも予測しなければならなくなっていきます。例えば永続化層にアクセスするクラスを開発しているのであれば、どんなに上層にあるレイヤーのクラスでも、テストデータをDBに入れないといけない、というのは、よくよく考えてみると、変な話なのです。どこかの段階で、DBを操作するクラスを参照しなくなるはずですから。大体、リズムが悪いですよね。DBの初期化用のテストデータ用意するのは大変です。
Unit Testでは、テストの対象を絞るべき。しかもできるだけ狭く。でも、該当するインタフェースを持ったスタブ*1を用意することなんてほとんどありません。だから、既存の実装を用いてテストを書く。すると、既存の実装とのインタラクションが発生し…(以下略)。
じゃー、どーするの?ってなったとき、僕等にはMockitoがあるわけですよ。具体的に実装を見ていきます。*2
具体的な例
ViewにModelを返却するControllerとしてこんなクラスを作ろうとしていたとします。(import文は省略)
BookController.java
public class BookController{ /** * 本のリストを取得して、book_list.htmlに表示する。 */ public ModelAndView getBookList(Request request){ return new ModelAndView("book_list"); } }
BookService.java
public interface BookService{ /** * 本のリストを取得する。 */ public List<Book> getBooks(); }
BookServiceImpl.java
public class BookServiceImpl implements BookService{ /** * DBからBookを取得してくる。 */ @Override public List<Book> getBooks(){ ... return result; // DBから取ってきた結果を返している } }
こんな感じの状態から、BookController.javaを対象にテストコードを書いていくとしましょうか。で、できるだけ、テストデータを用意しなくても済むように実装していきます。それでは正常系から通しましょうか。DBに2件くらいデータがあった状態でModelAndViewにモデルが2つ入っていることを検証するテストを書いていきます。
BookControllerTest.java (v1)
public class BookControllerTest{ @Test public void 本の一覧が取得できていること throws Exception{ BookController controller = new BookController(); ModelAndView mv = controller.getBookList(new Request()); List<Book> books = (List<Book>)mv.getModel("list"); assertThat(books.size(),is(2)); } }
こんな感じでテストコードを書いたとするじゃないですか。正常系で、データが2件くらいとれてて、ModelAndViewに登録されていたとします。じゃー、DBの実装もあることだし、テストデータを作って…。って、めんどい。おっしゃ、Mockito使ったるで!
BookControllerTest.java (v2)
public class BookControllerTest{ @Test public void 本の一覧が取得できていること throws Exception{ BookService service = mock(BookService.class); Book book = mock(Book.class); Book[] books = new Book[]{book,book}; // 横着してますけど、とりあえず2件とれればいいっすからね。 when(service.getBooks).thenReturn(Arrays.toList(books)); BookController controller = new BookController(); controller.setBookService(service); // DIパターンです。 ModelAndView mv = controller.getBookList(new Request()); // 取得しました。 List<Book> books = (List<Book>)mv.getModel("list"); assertThat(books.size(),is(2)); } }
これを通す実装は、
BookController.java
public class BookController{ private BookService service; /** * 本のリストを取得して、book_list.htmlに表示する。 */ public ModelAndView getBookList(Request request){ ModelAndView result = new ModelAndView("book_list"); List<Book> books = service.getBooks(); result.putModel("list",books); return result; } public void setBookService(BookService service){ this.service = service; } }
と、こんな感じになるんじゃないでしょうか。(最初からジャンプしてる感は勘弁してください。)実際、BookControllerクラスを実装している最中は、BookServiceの振る舞いは、そんなに関係ないはずですよね。*3なので、Mockitoでスタブを作ってUnit Testでは注入してみたわけです。誤解を招くかもしれないので、一応書いておきますけど、最終的にちゃんと動いているかどうか、はきちんと実装を組み上げた上でテストをするので、そこで担保します。あくまで、開発を素早く回すためにMockitoを使う訳です。
伝えたかったこと
結局お伝えしたかったのは、TDDでテストを書きつつ、実装を書こう、とした場合、できるだけ相手にする範囲を小さくしたいのに、テストデータをきっちり用意しなければならないのは、楽しくないです。Mockitoを使うようになってから、テストを書くのがさらに楽しくなりました。ここで書いた例は一番簡単な例だとは思いますが、使い始めてみると、英語ですがドキュメントがしっかりかかれているので、使いこなすのは難しくありません。( http://d.hatena.ne.jp/kompiro/20100227/1267286586 も合わせてどうぞ。)ぜひお試しください。