2013年6月23日日曜日

Android で mockito を使う : モックのメソッドが呼ばれたときの戻り値を指定する

public class MyClassA { MyClassB mMyClassB; public MyClassA(MyClassB b) { mMyClassB = b; } public void hoge() { if (mMyClassB.getPriority() > 0) { handleHighPriority(); } else { handleLowPriority(); } } public void handleHighPriority() { ... } public void handleLowPriority() { ... } } public class MyClassB { private int mPriority; public int getPriority() { return mPriority; } // 内部の処理で mPriority の値が決まる ... } MyClassA の hoge() が呼ばれたとき、MyClassB の priority に応じて対応するメソッドが呼ばれるかどうかテストします。

失敗するテスト public void testWhenHighPriority() { MyClassA mockMyClassA = mock(MyClassA.class); mockMyClassA.hoge(); verify(mockMyClassA, times(1)).handleHighPriority(); verify(mockMyClassA, never()).handleLowPriority(); } と書くと hoge() を呼んだ時点で NullPointerException が発生して handleHighPriority() が呼ばれずテストに失敗します。
mock() で作成した mockMyClassA では mClassB が null になっているからです。

そこで MyClassB をモックにし、それを MyClassA のコンストラクタに与えるようにします。

失敗するテスト public void testWhenHighPriority() { MyClassB mockMyClassB = mock(MyClassB.class); MyClassA myClassA = new MyClassA(mockMyClassB); myClassA.hoge(); verify(myClassA, times(1)).handleHighPriority(); verify(myClassA, never()).handleLowPriority(); } これで hoge() を呼んだ時点で NullPointerException が発生することは無くなりましたが、 org.mockito.exceptions.misusing.NotAMockException: Argument passed to verify() is of type MyClassA and is not a mock! が起こってテストに失敗します。 verify() に渡すインスタンスはモック化されていないといけないからです。 実際のインスタンスをモック化するには Mockito.spy() を使います。

失敗するテスト public void testWhenHighPriority() { MyClassB mockMyClassB = mock(MyClassB.class); MyClassA mockMyClassA = spy(new MyClassA(mockMyClassB)); mockMyClassA.hoge(); verify(mockMyClassA, times(1)).handleHighPriority(); verify(mockMyClassA, never()).handleLowPriority(); } これで NotAMockException が発生することは無くなりましたが、MyClassB の mPriority は初期値 = 0 のままなので、priority が 0 より大きいときのテストができません。

そこで mockito の機能を使います。
mockito では、「モックのあるメソッドが呼ばれたときにこの値を返す」という指定ができます。

"あるメソッドが呼ばれた" ということを指定するのが Mockito.when() メソッドです。
"そのときにこの値を返す" ということを指定するのが thenReturn() メソッドです。

MyClassB の getPriority() が呼ばれたときに 100 を返して欲しいなら when(mockMyClassB.getPriority()).thenReturn(100); のように書きます。 /** * MyClassA.hoge() を呼んだとき、MyClassB の priority が 100 なら * MyClassA.handleHighPriority() が呼ばれることを確認する */ public void testWhenHighPriority() { MyClassB mockMyClassB = mock(MyClassB.class); when(mockMyClassB.getPriority()).thenReturn(100); MyClassA myClassA = new MyClassA(mockMyClassB); myClassA.hoge(); verify(mockMyClassA, times(1)).handleHighPriority(); verify(mockMyClassA, never()).handleLowPriority(); } /** * MyClassA.hoge() を呼んだとき、MyClassB の priority が 0 なら * MyClassA.handleLowPriority() が呼ばれることを確認する */ public void testWhenLowPriority() { MyClassB mockMyClassB = mock(MyClassB.class); when(mockMyClassB.getPriority()).thenReturn(0); MyClassA myClassA = new MyClassA(mockMyClassB); myClassA.hoge(); verify(mockMyClassA, times(1)).handleLowPriority(); verify(mockMyClassA, never()).handleHighPriority(); } thenReturn(Integer value, Integer... values) のように第2引数が可変長引数になっているものも用意されています。
thenReturn(10, 20, 30, 100) のように指定すると、対応するメソッドが呼ばれるごとに返される値が後の引数になり、引数よりも呼ばれる回数が多くなったら一番最後の引数の値が返されます。



2013年6月22日土曜日

mockito を使ったテストでの java.lang.NoClassDefFoundError: org.mockito.Mockito を修正する

mockito を使ったテストプロジェクトでは、bin/dexedLibs フォルダに mockito や dexmacker の jar ができている必要があります。

「Android で mockito を使う : 準備編」 の手順では、libs フォルダに mockito や dexmaker の jar をコピーすると、次のテスト実行に bin/dexedLibs フォルダに mockito や dexmacker の jar ができます。

git リポジトリに Android プロジェクトを入れる場合、bin フォルダは入れないようにするでしょうから、git clone しただけだと次のテスト実行に bin/dexedLibs フォルダに mockito や dexmacker の jar ができず、テスト時に java.lang.NoClassDefFoundError: org.mockito.Mockito エラーが発生します。

この現象は mockito だけでなく、libs に jar を入れているプロジェクトなら起こりえます。

これを修正するには
  • テストプロジェクトの Properties で [Java Build Path] → [Order and Export] で Android Private Libraries のチェックボックスをチェック
  • テストプロジェクトを clean
(mockito と dexmacker の jar は libs フォルダに入ったときに Android Private Libraries に含まれるようになります)

をします。

こうすると次のテスト実行に bin/dexedLibs フォルダに mockito や dexmacker の jar ができ、java.lang.NoClassDefFoundError: org.mockito.Mockito エラーが起こらなくなります。



2013年6月21日金曜日

Android で mockito を使う : メソッド呼び出し時の引数をチェックする

JSON をパースして、リスナーの対応するメソッドを呼び出すユーティリティメソッドがあるとします。
"Status" というキーの値(文字列)とそのときの時間(long)を handleStatus() の引数として渡すようになっています。 public class Utils { public interface ResultListener { void onError(); void handleStatus(String status, long time); } public static void handleJson(String json, ResultListener listener) { if (listener == null) { return; } try { JSONObject obj = new JSONObject(json); String status = obj.optString("Status"); listener.handleStatus(status, System.currentTimeMillis()); } catch (JSONException e) { listener.onError(); e.printStackTrace(); } } } まずメソッドが呼ばれるかどうかをテストしましょう。
前回紹介したように Mockito.verify() メソッドを使います。 /** * Status キーがある場合、handleJson() が呼ばれることを確認する */ public void testStatusKey() { Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).handleStatus(anyString(), anyLong()); } handleStatus() は引数として String と long をとるので verify(mockResultListener, only()).handleStatus(); のように指定することはできません。
そこで、「引数の文字列がどんな値であっても気にしない、メソッドが呼び出されているかどうかだけ確かめたい」という場合、anyString() や anyLong() を指定することができます。
String, long 以外にも anyInt(), anyBoolean() などが用意されています。

引数の値をチェックするために、引数に直接文字列を指定することができます。

Status キーの値は正しい値を指定することができますが、long の引数は handleStatus() 内で System.currentTimeMillis() しているため、次のテストは失敗します。

失敗するテスト /** * Status キーがある場合 handleJson() が呼ばれ、引数が Status キーの値であることを確認する */ public void testStatusKey() { Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).handleStatus("hoge1", System.currentTimeMillis()); } そこで long は値を検証しないことにして、anyLong() を使って次のように書くとまたまたテストに失敗します。

失敗するテスト /** * Status キーがある場合 handleJson() が呼ばれ、引数が Status キーの値であることを確認する */ public void testStatusKey() { Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).handleStatus("hoge1", anyLong()); } 実は any〇〇() と実際の値を並記することはできません。any〇〇() を使う場合、実際の値の部分には eq() を使います。 /** * Status キーがある場合 handleJson() が呼ばれ、引数が Status キーの値であることを確認する */ public void testStatusKey() { Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).handleStatus(eq("hoge1"), anyLong()); }


Status キーの値は optString() で取得しているため、Status キーが無い場合は空文字になります。

失敗するテスト /** * Status キーが無い場合、handleJson() が呼ばれ引数が空文字であることを確認する */ public void testNoStatusKey() { Utils.handleJson("{\"State\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).handleStatus(eq(""), anyLong()); }



引数をログに出力したいということがあるでしょう。そのためには verify 時に引数をキャッチしておく必要があります。
これを行ってくれるのが ArgumentCaptor です。

ここでは String と long の引数をキャッチしたいので ArgumentCaptor<String> statusCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<Long> timeCaptor = ArgumentCaptor.forClass(Long.class); のようにして ArgumentCaptor のインスタンスを作ります。

あとはこの ArgumentCaptor の capture() を引数として渡します。 /** * Status キーがある場合 handleJson() が呼ばれ、引数が Status キーの値であることを確認する */ public void testStatusKey() { Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); ArgumentCaptor<String> statusCaptor = ArgumentCaptor .forClass(String.class); ArgumentCaptor<Long> timeCaptor = ArgumentCaptor.forClass(Long.class); verify(mockResultListener, only()).handleStatus(statusCaptor.capture(), timeCaptor.capture()); String status = statusCaptor.getValue(); long time = timeCaptor.getValue(); assertEquals("hoge1", status); System.out.println("Status = " + status + ", time = " + time); } verify() 後は ArgumentCaptor の getValue() で値を取ることが出来ます。


ArgumentCaptor.capture() と any〇〇() は並記することができます verify(mockResultListener, only()).handleStatus(anyString(), timeCaptor.capture()); 一方、直接引数を渡す場合とは並記できません。

失敗するテスト verify(mockResultListener, only()).handleStatus("hoge1", timeCaptor.capture()); eq() を使えば大丈夫です。 verify(mockResultListener, only()).handleStatus(eq("hoge1"), timeCaptor.capture());




Android で mockito を使う : メソッドの呼び出しをチェックする

JSON をパースして、リスナーの対応するメソッドを呼び出すユーティリティメソッドがあるとします。

public class Utils { public interface ResultListener { void onError(); void onHoge1(); void onHoge2(); } public static void handleJson(String json, ResultListener listener) { if (listener == null) { return; } try { JSONObject obj = new JSONObject(json); String status = obj.optString("Status"); if (status.equals("hoge1")) { listener.onHoge1(); } else if (status.equals("hoge2")) { listener.onHoge2(); } } catch (JSONException e) { listener.onError(); e.printStackTrace(); } } }

このメソッドをテストするには、JSON文字列を渡して、対応するリスナーのメソッドがちゃんと呼ばれるかどうか確認すればいいわけです。

まず、テストプロジェクトにテストしたいクラスと同じパッケージを作ります。

ここではテスト対象クラス(Utils)が com.example.mockitosample なので、テストプロジェクトの MockitoSampleTest にも com.example.mockitosample を作ります。



テストプロジェクトの com.example.mockitosample を選択して右クリックし、[New] → [JUnit Test Case] を選択します。
Name にテストクラス名を入れて Finish をクリックします。
テスト対象クラス名 + Test にすることが多いです。





このユーティリティメソッドの動作パターンとして以下があります。
  • 1. 引数で渡す文字列が JSON 文字列として正しくない場合、onError() が呼ばれ他のメソッドは呼ばれない
  • 2. (引数で渡す)JSON 文字列に Status キーが無い場合、onError() も他のメソッドも呼ばれない
  • 3. JSON 文字列の Status キーの値が hoge1 でも hoge2 でもない場合、onError() も他のメソッドも呼ばれない
  • 4. JSON 文字列の Status キーの値が hoge1 の場合、onHoge1() が呼ばれ他のメソッドは呼ばれない
  • 5. JSON 文字列の Status キーの値が hoge2 の場合、onHoge2() が呼ばれ他のメソッドは呼ばれない
ということでテストメソッドは5つです。
JUnit3 では test で始まるメソッドがテストメソッドとして認識されます。



まず、「1. 引数で渡す文字列が JSON 文字列として正しくない場合、onError() が呼ばれ他のメソッドは呼ばれない」 のテストメソッドを作ってみます。 /** * 引数で渡す文字列が JSON 文字列として正しくない場合、 onError() が呼ばれ他のメソッドは呼ばれないことを確認する */ public void testInvalidJson() { }

mockito を使わない場合「グローバル変数を用意してリスナーのメソッドで変数の値を変えて、どれが呼ばれたかチェックする」みたいなことをしますが、mockito を使うともっとスマートにできます。

まず、メソッドが呼ばれたかチェックしたいクラスをモック化します。
ここでは ResultListener のメソッドが呼ばれたかチェックしたいので ResultListener をモック化します。

モック化には Mockito.mock() を使います。
他にも Mockito 下の static method をたくさん使うので

import static org.mockito.Mockito.*;

を入れておくといいでしょう。

ResultListener mockResultListener = mock(ResultListener.class); あとはこのモックを使ってテストしたいメソッドを呼び出します。 ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("", mockResultListener);

ここでは JSON 文字列として不適切な空文字を渡しているので onError() が呼ばれるはずです。
それをチェックするのが verify() メソッドです。 ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("", mockResultListener); verify(mockResultListener).onError();

onHoge1() と onHoge2() は呼ばれないはずです。呼ばれなかったかどうかをチェックするには、verify() メソッドの第2引数に never() を指定します。 ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("", mockResultListener); verify(mockResultListener).onError(); verify(mockResultListener, never()).onHoge1(); verify(mockResultListener, never()).onHoge2();

これだと例えば onHoge3() メソッドが増えたときに修正が必要になります。
verify() メソッドの第2引数に only() を付けることで onError() しか呼ばれていないということをチェックできます。 ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("", mockResultListener); verify(mockResultListener, only()).onError();

テストメソッドの全体はこうなります。 /** * 引数で渡す文字列が JSON 文字列として正しくない場合、 onError() が呼ばれ他のメソッドは呼ばれないことを確認する */ public void testInvalidJson() { ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("", mockResultListener); verify(mockResultListener, only()).onError(); }



「2. (引数で渡す)JSON 文字列に Status キーが無い場合、onError() も他のメソッドも呼ばれない」のテストを作ってみましょう。

これ以降になにも行われないことをチェックするメソッドとして verifyNoMoreInteractions() があります。
ここでは全てのメソッドが呼ばれないのでこれを使うことが出来ます。 /** * Status キーが無い場合、どのメソッドも呼ばれないことを確認する */ public void testNoStatusKey() { ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("{\"State\":\"hoge1\"}", mockResultListener); verifyNoMoreInteractions(mockResultListener); }



「3. JSON 文字列の Status キーの値が hoge1 でも hoge2 でもない場合、onError() も他のメソッドも呼ばれない」のテストを作ってみましょう。

引数で渡す文字列が変わるだけで 2. と一緒ですね /** * Status キーの値が hoge1 でも hoge2 でも無い場合、どのメソッドも呼ばれないことを確認する */ public void testInvalidStatusValue() { ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("{\"Status\":\"hoge3\"}", mockResultListener); verifyNoMoreInteractions(mockResultListener); }



「4. JSON 文字列の Status キーの値が hoge1 の場合、onHoge1() が呼ばれ他のメソッドは呼ばれない」のテストを作ってみましょう。 /** * Status キーの値が hoge1 の場合、onHoge1() が呼ばれ他のメソッドは呼ばれないことを確認する */ public void testHoge1() { ResultListener mockResultListener = mock(ResultListener.class); Utils.handleJson("{\"Status\":\"hoge1\"}", mockResultListener); verify(mockResultListener, only()).onHoge1(); }

「5. JSON 文字列の Status キーの値が hoge2 の場合、onHoge2() が呼ばれ他のメソッドは呼ばれない」のテストは 4. とほぼ同じなので省略します。



ResultListener をモック化する処理 ResultListener mockResultListener = mock(ResultListener.class); は各メソッドで行っているので、mockResultListener をフィールドとして保持するようにして setup() メソッドでモック化させると、このテストクラスがどのモックに依存しているかわかりやすくなります。 public class UtilsTest extends TestCase { ResultListener mockResultListener; @Override protected void setUp() throws Exception { super.setUp(); mockResultListener = mock(ResultListener.class); } /** * 引数で渡す文字列が JSON 文字列として正しくない場合、 onError() が呼ばれ他のメソッドは呼ばれないことを確認する */ public void testInvalidJson() { Utils.handleJson("", mockResultListener); verify(mockResultListener, only()).onError(); } ... }

5つともグリーンになりました。







verify() の第2引数に times(int) を渡すことで何回呼ばれたかをチェックすることができます。
第2引数を指定しない場合は times(1) を指定したのと同じことになります。

verify(mockResultListener, times(2).onHoge3();



Android で mockito を使う : 準備編

mockito はテストのためのモックフレームワークです。



1. mockito の準備

mockito のプロジェクトページ(https://code.google.com/p/mockito/)に行って、最新の(ここでは mockito-1.9.5.zip)をダウンロードします。





ダウンロードした zip を展開して、中に jar ファイル(ここでは mockito-all-1.9.5.jar)が入っていることを確認します。



2. dexmaker の準備

Android で mockito を使うには dexmaker も必要です。
dexmaker のプロジェクトページ(http://code.google.com/p/dexmaker/)に行って、dexmaker-1.0.jar と dexmaker-mockito-1.0.jar をダウンロードします。





3. テストプロジェクトの準備

MockitoSample というプロジェクトがあるとします。



このプロジェクト用のテストプロジェクトを作成するには

[File] → [New] → [Other...] → [Android] → [Android Test Project]

を選択し、



テストプロジェクトの名前を入力し(テスト対象のプロジェクト名 + Test とすることが多い)、



Test Target にテスト対象のプロジェクトを選択します。



テスト用のプロジェクトが作成されました。



テストプロジェクトのトップに libs フォルダを作成し、ダウンロードした mockito-all-1.9.5.jar, dexmaker-1.0.jara, dexmaker-mockito-1.0.jar を入れます。





4. static import の設定

設定の [Java] → [Editor] → [Content Assist] → [Favorites] に
org.mockito.Matchers.*
org.mockito.Mockito.*
を追加(入ってなかったら)



設定の [Java] → [Code Style] → [Organize Imports] の
Number of static imports needed for .* の数字を 1 に変更





2013年6月4日火曜日

In-app billing V3 は singleInstance な Activity では使えない

「Android launchMode の違い」 で解説したように、Activity には4つの launchMode が設定できます。
  • standard
  • singleTop
  • singleTask
  • singleInstance
それぞれの解説は上のエントリーに任せるとして、このなかの singleInstance を指定した Activity で In-app billing V3 の購入処理を行うとうまくいきません。

singleInstance はアプリでタスク1個、タスク内の Activity も1個というストイックな設定です。

In-app billing V3 の課金処理では、Activity の startIntentSenderForResult (IntentSender intent, int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) を使って購入フローの Activity を起動し、結果を onActivityResult (int requestCode, int resultCode, Intent data) で受けとります。 Result を受けとるため、起動した Activity は同じタスクに入る必要があります。

singleInstance だとタスク内の Activity は1つだけなので、購入フローの Activity は別のタスクとして起動されます。別タスクだと結果が受けとれないので、購入フローの Activity を呼び出した直後に responseCode = RESULT_CANCELED, data = null で onActivityResult() が呼ばれます。

購入フローでは、購入処理が終わったあとに onActivityResult() が呼ばれ、data に購入処理の結果が入ります。このため、singleInstance だと購入情報が受けとれないということになってしまうのです。

Logcat でも下のように購入フロー Activity の呼び出し直後に「Activity is launching as a new task, so cancelling activity result.」と言われています。

06-04 11:59:57.619: I/ActivityManager(533): START u0 {cmp=com.android.vending/com.google.android.finsky.billing.lightpurchase.IabV3Activity (has extras)} from pid -1
06-04 11:59:57.619: W/ActivityManager(533): Activity is launching as a new task, so cancelling activity result.


購入フロー以外の処理、例えば、アイテムの詳細を取得(getSkuDetails())や購入済みアイテムの一覧を取得(getPurchases())やアイテムの消費(consumePurchase ())は普通にできます。



まぁ、singleInstance にするアプリなんてそうそうないと思うけど。