読者です 読者をやめる 読者になる 読者になる

Mockitoノススメ テストスタイルの変化

Mockito テスト TDD Mock

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 も合わせてどうぞ。)ぜひお試しください。

*1:ここでは同一インタフェースを持った空実装と解釈してください

*2:下記の実装はてきとーに書いたコードなので、実際に動作するとは限りません。あくまで思考の家庭を示しただけです。

*3:API上、約束事が仕様として記述されているのであれば、それはそれで大切ですよ。