GeekFactory

int128.hatenablog.com

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:そうでないメソッドもあるかも。