2013年6月21日金曜日

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

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

  1. public class Utils {  
  2.   
  3.     public interface ResultListener {  
  4.   
  5.         void onError();  
  6.   
  7.         void onHoge1();  
  8.   
  9.         void onHoge2();  
  10.     }  
  11.   
  12.     public static void handleJson(String json, ResultListener listener) {  
  13.         if (listener == null) {  
  14.             return;  
  15.         }  
  16.   
  17.         try {  
  18.             JSONObject obj = new JSONObject(json);  
  19.   
  20.             String status = obj.optString("Status");  
  21.   
  22.             if (status.equals("hoge1")) {  
  23.                 listener.onHoge1();  
  24.   
  25.             } else if (status.equals("hoge2")) {  
  26.                 listener.onHoge2();  
  27.             }  
  28.   
  29.         } catch (JSONException e) {  
  30.             listener.onError();  
  31.             e.printStackTrace();  
  32.         }  
  33.     }  
  34. }  


このメソッドをテストするには、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() が呼ばれ他のメソッドは呼ばれない」 のテストメソッドを作ってみます。
  1. /** 
  2.  * 引数で渡す文字列が JSON 文字列として正しくない場合、 onError() が呼ばれ他のメソッドは呼ばれないことを確認する 
  3.  */  
  4. public void testInvalidJson() {  
  5. }  


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

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

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

import static org.mockito.Mockito.*;

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

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


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


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


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


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




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

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




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

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




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


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



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


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







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

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




1 件のコメント:

  1. こんばんは。

    呼ばれたかどうかのテスト、便利ですね。

    返信削除