Hatena::ブログ(Diary)

Fly me to the Juno! このページをアンテナに追加 RSSフィード

2010-03-18

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

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

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

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

2010-02-27

Mockitoノススメ

モックライブラリ使ってますか?

僕はJavaの人なので、主にJUnitを使ってテストコードを書いています。テストコードを書いている最中、「もしこのオブジェクトから例外が帰ってきたら、ちゃんと例外のハンドリングができてんの?」等々、既存のオブジェクトの振る舞いを差し替えたくなることってありませんか?そういうときにモックライブラリを使うと、既存のオブジェクト処理を差し替える事ができます。

実は最初はモックライブラリって意味あるの?と懐疑的だったんです。どういうところに懐疑的だったかというと、

  • テストコード中に出てくるモックライブラリのセットアップがめんどい。
  • テストコードがプロダクトコードの実装に依存しちゃうんじゃないの?プロダクトコードをちょっと変えただけでテストが落ちるようになるんじゃないの?

みたいなところです。でもMockitoというモックライブラリを使ってテストコードを書き初めてからその態度は変わりました。Mockitoを使ってテストを書くと、次の効果が見込まれます。

  • テストコードに対応するプロダクトコードの中のロジックのみに関心を持ったテストコードを記述できるようになります。
  • スタブとして差し替えたインタフェースへの期待値が、そのままテストケースとして追加できます。
  • 遅い処理を持ったオブジェクトをスタブへ差し替えられるので、スローテスト問題へ立ち向かう事ができます。
  • モックとして差し替えた場合、プロダクトコード内で呼び出されていることを検証できます。
  • オブジェクトをラップすることで、一部分だけ処理をスタブするパーシャルモック(部分モック)があます。

スタブとかモックの用語は、どちらもテスト用代替オブジェクトとして使うため、こうしてみると大きな差ではない気がするんですが、テスト信奉者の方々からすると結構重要な事として位置づけられるので、覚えておいた方がいいです。

Mockitoの使い方

それではMockitoの使い方ですが、簡単です。まず、モックオブジェクトの作り方。基本一行です。

  List list = mock(List.class);

これでListクラスのモックオブジェクトの作成が完了です。続いて先にモックオブジェクトを使ってモックする場合、どうするかですが、もし、listにあるオブジェクトが追加されている事を検証したいとしましょう。そういうときはこう書きます。

  List list = mock(List.class);
  target.setList(list); // モックオブジェクトに差し替える
  target.execute();  // テストしたいメソッドを実行する。
  verify(list).add(eq("test")); // listにtestというStringが追加されている事を検証する。

eq("test")は、obj.equals("test")と同じような感じで、実行したときの引数に渡ってきた値を検証します。もしどんな値でもいいのであれば、any()メソッドを使ってください。any()メソッドを使う場合はキャストをしないと、コンパイルエラーになります。

もし2回以上実行されている事を確認したい場合は

  List list = mock(List.class);
  target.setList(list); // モックオブジェクトに差し替える
  target.execute();  // テストしたいメソッドを実行する。
  verify(list,times(2)).add(eq("test")); // listにtestというStringが2回実行された事を検証する。

逆に中で追加されていない場合はnever()を使います。

  List list = mock(List.class);
  target.setList(list); // モックオブジェクトに差し替える
  target.execute();  // テストしたいメソッドを実行する。
  verify(list,never()).add(eq("test")); // listにtestというStringが追加されていない事を検証する。

続いて、スタブの方法ですが、こんな感じ。

  List list = mock(List.class);
  when(list.get(eq(0))).thenReturn(new Object()); //list.get(0)が呼ばれたらObjectを返す。

簡単ですね。もし、判断させたい場合は、

  List mock = mock(List.class);
  when(mock.get(anyInt())).thenAnswer(new Answer<Object>() {
    public Object answer(InvocationOnMock invocation) throws Throwable {
      Object arg = invocation.getArguments()[0];
      Integer index = (Integer) arg;
      if(index < 10){
        return new Object();
      }
      return null;
    }
  });

のようにAnswerクラスを使います。途中出てきたanyInt()は、どのintでもOKということですね。

続いて何らかの例外を投げさせたければ

  List mock = mock(List.class);
  when(mock.get(anyInt())).thenThrow(new IndexOutOfBoundsException());

というように、thenThrowを使ってやってくだし。

ほんで、返値のないメソッドのスタブをさせたい場合は、

  @Test(expected=UnsupportedOperationException.class)
  public void unmodifiable() throws Exception {
    List mock = mock(List.class);
    doThrow(new UnsupportedOperationException()).when(mock).add(any());
    mock.add(new Object());
  }

と言う感じで、doなんたらメソッドを使います。

ほんで、パーシャルモックの作成はこんな感じ。

  List list = new ArrayList();
  List mock = spy(list);
  doReturn(new Object()).when(mock).get(0);

とかすると、mock.get(0)で期待した通り、オブジェクトが返ってきます。

最後にQuick JUnitで実験的にMockitoをサポートするプラグインを作成しました。Quick JUnit Mockito Integration (Experimental)として配布中です。このプラグインをインストールすると、Qというコンテンツアシストで、

f:id:kompiro:20100228005955p:image

f:id:kompiro:20100228005948p:image

こんな感じでMockito関連のインポートだけじゃなく、hamcrestのCoreMatcherもインポートしているんで、便利でござる。

http://quick-junit.sourceforge.jp/updates/beta/ で配布中なので使ってみてくだし。