Picasa APIを利用するコードのユニットテスト
Picasa Web Albums APIのクライアントライブラリを利用するコードのユニットテストをどう実現するか考えてみました。結論としてはモックを使った普通のテストになりました。
- テスト対象はドメインロジックとする。Picasaクライアントライブラリの実装にテストコードが依存しないようにする。
- テストはオフラインで行う。Picasa APIには接続しない。
- テストデータはテストコードの実行時に生成する。
ここでは、モデルクラスはPicasaクライアントライブラリのモデルクラスを意味します。例えば、AlbumEntry, PhotoEntry, UserFeedなどです。
モデルクラスのgetter, setterのみに依存するコード
モデルクラスのgetter, setterはメモリ上のデータを操作します*1。したがって、getter, setterのみに依存するコードはインスタンスを利用してテストできます。例えば、アルバムタイトルを検索するコードをテストするには、AlbumEntryをnewしてsetTitle()でタイトルをセットし、テスト対象コードに渡します。
public class Albums extends ArrayList<AlbumEntry> { /** * Find an album by title. * @param title * @return {@link AlbumEntry} or null if not found */ public AlbumEntry findTitle(String title) { if (title == null) { throw new NullPointerException("title is null"); } for (AlbumEntry entry : this) { String entryTitle = entry.getTitle().getPlainText(); if (title.equals(entryTitle)) { return entry; } } return null; } }
public class AlbumsTest { @Test public void findTitle_found() { Albums albums = new Albums(); AlbumEntry a1 = new AlbumEntry(); a1.setTitle(TextConstruct.plainText("2011-08-31")); albums.add(a1); AlbumEntry a2 = new AlbumEntry(); a2.setTitle(TextConstruct.plainText("2011-09-01")); albums.add(a2); AlbumEntry actual = albums.findTitle("2011-08-31"); assertThat(actual, is(sameInstance(a1))); } }
もしくは、モデルクラスのモックを生成します。Mockitoを使うと下記のようになります。先ほどはテストデータを生成しましたが、下記では振る舞いを規定しています。テストコードが読みやすい方を採用するとよいでしょう。
@Test public void testSomething() throws Exception { final long now = new Date().getTime(); Albums albums = new Albums(); AlbumEntry a1 = mock(AlbumEntry.class); when(a1.getPublished()).thenReturn(new DateTime(now - 1 * 86400000L)); albums.add(a1); // ... }
モデルの更新、削除
モデルの追加、更新、削除はPicasa APIにリクエストを発行するため、サービスのスタブが必要になります。更新や削除であれば、モックを利用してupdate(), delete()メソッドが呼ばれたことを確認するのが簡単です。
@Test public void testDeleteOld_1day() throws Exception { TimeZoneLocator.set(TimeZone.getTimeZone("UTC")); final long now = new Date().getTime(); Albums albums = new Albums(); AlbumEntry a0 = mock(AlbumEntry.class); when(a0.getPublished()).thenReturn(new DateTime(now - 0 * 86400000L)); albums.add(a0); AlbumEntry a1 = mock(AlbumEntry.class); when(a1.getPublished()).thenReturn(new DateTime(now - 1 * 86400000L)); albums.add(a1); AlbumEntry a2 = mock(AlbumEntry.class); when(a2.getPublished()).thenReturn(new DateTime(now - 2 * 86400000L)); albums.add(a2); albums.deleteOld(1); // 1日前より昔のアルバムを削除する assertThat(albums.size(), is(2)); assertThat(albums.contains(a0), is(true)); assertThat(albums.contains(a1), is(true)); verify(a0, never()).delete(); // 今日のアルバムは削除されない verify(a1, never()).delete(); // 昨日のアルバムは削除されない verify(a2).delete(); // 一昨日のアルバムは削除される }
モデルの取得、追加
モデルの追加については PicasawebService#insert() のモックを生成します。テストに必要な振る舞いのみ記述するとよいでしょう。
@Test public void findOrCreateDateAlbum_notfound() throws Exception { // insert method returns its argument as is. when(service.insert((URL) any(), (AlbumEntry) any())).thenAnswer(new Answer<AlbumEntry>() { @Override public AlbumEntry answer(InvocationOnMock invocation) throws Throwable { AlbumEntry e = (AlbumEntry) invocation.getArguments()[1]; e.setId("https://albums/999"); // アルバムIDを採番する return e; } }); Albums.findOrCreate(something); // 存在しない場合はアルバムを追加する verify(service).insert((URL) any(), eq(actual)); }
上記と同様に、モデルの取得については PicasawebService#getFeed() のモックでテスト可能です。
あとがき
コントローラ層のテストでは、サービスクラスはDIするか、Service Locatorでテスト実行時に入れ替えるなりでよいと思います。
モックを作るより良い方法をご存じでしたら教えてください。実は、Picasaクライアントライブラリにはローカルテスト用のサービススタブが含まれているとか。App Engineのデータストアみたいに簡単にテストできるといいですね。
*1:そうでないメソッドもあるかも。