PowerMockによるAndroidユニットテストとBDD動作駆動開発

12094 ワード

ずっと前からmockitoとPowerMockの名前を聞いたことがありますが、仕方なく当社の書き込みユニットテストの風潮は濃くありません.加えて、これまで業務が忙しくて、惰性で私はずっと書き込みユニットテストの習慣を持っていません.
ちょうど今手元にあるのは新しいプロジェクトで、初期に時間があっても衝動的にいろいろな必要なものを使うことができます.そこでここ数日よく勉強して、PowerMockは確かにこの上なく強いと感じました.
モックって何?
ウィキペディアにはこう書かれています.
オブジェクト向けプログラミングでは、シミュレーションオブジェクト(英語:mock object、模倣オブジェクトとも訳す)は、制御可能な方法で実際のオブジェクトの動作をシミュレートする偽のオブジェクトである.プログラマは通常、他のオブジェクトの動作をテストするためにシミュレーションオブジェクトを作成します.自動車設計者が衝突テストの偽人を使用して車両の衝突中の人の動的挙動をシミュレートするのと似ています.ユニットテストでは、シミュレーションオブジェクトは複雑でリアル(非シミュレーション)なオブジェクトの動作をシミュレートできます.実際のオブジェクトがユニットテストに入れられない場合は、シミュレーションオブジェクトを使用すると便利です.
依存注入を使用してコードを記述する場合、例えばContextは通常外部から送信されます.
class ClassA{
    public static boolean staticFunc(Context context, int arg) {
        ...
    }
}
この方法はmock objectの方法を用いなければ,Contextがシステム生成であるため,アンドロイド環境から離れてユニットテストを記述することは困難である.
mockテクノロジーを使用してContextをシミュレートすると、android studioでAndroid環境実行ユニットテストを記述し、離脱することができます.
PowerMock
Powermockは流行のjava mockフレームワークであり、シミュレーションオブジェクトを簡単に実現することができます.実際にはEasyMock、Mockitoなどの他の流行の枠組みを継承し、拡張しています.
Android studioにpowermockフレームワークを導入するのは簡単で、build.gradleにdependenciesを追加すればいいです.
dependencies {
    ...
    testCompile 'junit:junit:4.12'

    testCompile 'org.powermock:powermock-core:1.6.1'
    testCompile 'org.powermock:powermock-module-junit4:1.6.1'
    testCompile 'org.powermock:powermock-module-junit4-rule:1.6.1'
    testCompile 'org.powermock:powermock-api-mockito:1.6.1'
}
ここに穴があります.以前は1.5.6バージョンのpowermockを使っていましたが、私のjunitは4.12バージョンなので、@RunWith(PowerMockRunner.class)を使っているときにエラーが発生します.
org.powermock.reflect.exceptions.FieldNotFoundException: Field 'fTestClass' was not found in class org.junit.internal.runners.MethodValidator.
stackoverflowで海外の大神を検索した答えはpowermockが1.6.1未満のバージョンでjunit 4.12のバグを使用して1.6.1で修復された.従ってjunit 4.12+powermock 1.6.1を用いるか、junit 4.11+powermock 1.5.6を用いる.
mockの簡単な使い方
まず、最近私が手に入れた需要について話します.私たちのアプリケーションのボタンをクリックして他のアプリケーションの応答を起動するには、サーバ上で構成する必要があります.サーバには、パッケージ名起動アプリケーション、action起動アプリケーション、Uri起動アプリケーションが割り当てられている可能性があります.
ツールクラスをこう書きました
public class AppUtils {
    public static boolean startApp(Context context, StartAppParam param) {
        ...
    }
    
     public static class StartAppParam {
        private String packageName;
        private String activity;
        private String action;
        private String uri;
        private List categorys = new ArrayList<>();
        ...
    }
}
サーバ上にjsonを配置し、クライアントに転送してStartAppParamとして解析し、AppUtilsを呼び出す.startAppメソッド.これでこのニーズを実現できます.
TDD方式を用いてこの機能を開発した.まず、Actionのみを構成する方法で起動することを考慮します.
@Test
    public void testOpenAppByAction() {
        Context context = Mockito.mock(Context.class);
        
        AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam.class);
        PowerMockito.when(param.getAction()).thenReturn("package");
        
        assertTrue(AppUtils.startApp(context, param));
            
        Mockito.verify(context, Mockito.times(1)).startActivity(Matchers.any(Intent.class));
    }
まず、Mockitoを使用する.mockメソッドは、シミュレーションオブジェクトを作成することができます.ここではシミュレーションのContextを使用してandroid studioでユニットテストを直接実行できます.
同時にparamもmockで作成され、getActionメソッドもシミュレートされ、Actionを使用してアプリケーションを起動するように構成されていることを示す「package」を返します.
AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam .class);
PowerMockito.when(param.getAction()).thenReturn("package");
そしてverifyメソッドは、呼び出したメソッドの呼び出し回数を検証するために使用できます.たとえば、startActivityが1回呼び出されたことを検証します.
mockメソッド内部で作成されたオブジェクト
もちろん、このテストは十分ではありません.Actionによって起動されたかどうかを検証していないからです.つまり、new Intent(param.getAction()によってIntentが作成されたかどうかを判断する必要があります.
これはPowerMockの非常にキックアスな機能を使用して、それは外部のmockの1つのオブジェクトでパラメータを通じてテストする必要がある方法に伝えることができるだけではなくて、更に直接mockの方法の内部で作成したオブジェクト(例えばここのIntent)をmockすることができます!
@RunWith(PowerMockRunner.class)
public class AppUtilsTest {

    @Test
    @PrepareForTest({AppUtils.class})
    public void testOpenAppByAction() throws Exception {
        Intent intent = Mockito.mock(Intent.class);
        PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);

        Context context = Mockito.mock(Context.class);

        AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam.class);
        PowerMockito.when(param.getAction()).thenReturn("package");

        assertTrue(AppUtils.startApp(context, param));
        
        Mockito.verify(context, Mockito.times(1)).startActivity(intent);
        Mockito.verify(intent, Mockito.times(0)).setData(Matchers.any(Uri.class));
        Mockito.verify(intent, Mockito.times(0)).addCategory(Matchers.anyString());
        Mockito.verify(intent, Mockito.times(0)).setClassName(Matchers.anyString(), Matchers.anyString());
    }
}
まず@RunWith(PowerMockRunner.class)でAppUtilsTestクラスを注釈し、@PrepareForTest({AppUtils.class})でtestOpenAppByActionメソッドを注釈し、入力AppUtils.classは、AppUtilsクラス内でmock操作を実行する必要があることを示します.
次にmockは1つのIntentを出て、それから以下の方法を使ってnew Intent(「package」)を使って得たIntentが私たちのmockから出たintentであるようにして、ここで入力した「package」パラメータも一致してこそ私たちのmockから出たintentを得ることができることに注意します.そうでなければnullしか得られません.
PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);
したがって、startActivity呼び出しのintentがmockから出たオブジェクトであるかどうかを検証するだけで、Actionによって起動されたアプリケーションであるかどうかを検証することができます.
Mockito.verify(context, Mockito.times(1)).startActivity(intent);
もちろん、保険のためにIntentの他の方法が呼び出されていないかどうかを確認することができます.
Mockito.verify(intent, Mockito.times(0)).setData(Matchers.any(Uri.class));
Mockito.verify(intent, Mockito.times(0)).addCategory(Matchers.anyString());
Mockito.verify(intent, Mockito.times(0)).setClassName(Matchers.anyString(), Matchers.anyString());
BDD方式でユニットテストを作成する
BDD(Behavior-driven development,行動駆動開発)は,非プログラマー可読のテスト用例を自然言語で書くことによってテスト駆動開発方法を拡張した.つまりbdd方式で書かれたコードはプログラマーでない人でも読めるので、この可読性の重要性は私が口を酸っぱくしなくてもいいのではないでしょうか.
実はMockitoのBDD方式の書き方は特に自然言語のようなものではないと思います.C++のユニットテストフレームCatchフレームワークを例に挙げたいと思います.
GIVEN("a enable stub publish server entry") {
    StubPublishServerEntry entry(true);
    entry.Start();

    WHEN("publish service") {
        entry.PublishService(service, on_result, on_success, on_error);

        THEN("publish successfully") {
            REQUIRE(service_entry != nullptr);
            REQUIRE(service_entry->IsPublished());
            REQUIRE(is_on_success);
            REQUIRE_FALSE(is_on_error);
        }
    }
}
これは私の前の半製品プロジェクトのコードクリップです.コード部分を外して、GIVEN、WHEN、THENの3つのマクロの中のものだけを残しておくと、基本的には英語がわかる人だけがこのコードが何をしたいのか理解できます.
GIVEN("a enable stub publish server entry") {
    ...
    WHEN("publish service") {
        ...
        THEN("publish successfully") {
            ...  
        }
    }
}
PowerMockもBDDに対応しており(MockitoはBDDに対応していると言うべき)、上記のテスト例をBDDの書き方に変更することができます.
public void testOpenAppByAction() throws Exception {
    Intent intent = Mockito.mock(Intent.class);
    PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);

    Context context = Mockito.mock(Context.class);

    AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam .class);

    //given
    BDDMockito.given(param.getAction()).willReturn("package");

    //when
    assertTrue(AppUtils.startApp(context, param));

    //then
    BDDMockito.then(context).should().startActivity(intent);
    BDDMockito.then(intent).should(Mockito.never()).setData(Matchers.any(Uri.class));
    BDDMockito.then(intent).should(Mockito.never()).addCategory(Matchers.anyString());
    BDDMockito.then(intent).should(Mockito.never()).setClassName(Matchers.anyString(), Matchers.anyString());
}
自然言語との差が大きいような気がしますが、いくつかの方法をimport staticを通じてimportに変更しました.
public void testOpenAppByAction() throws Exception {
    Intent intent = mock(Intent.class);
    whenNew(Intent.class).withArguments("package").thenReturn(intent);

    Context context = mock(Context.class);

    AppUtils.StartAppParam param = mock(AppUtils.StartAppParam .class);

    //given
    given(param.getAction()).willReturn("package");

    //when
    assertTrue(AppUtils.startApp(context, param));

    //then
    then(context).should().startActivity(intent);
    then(intent).should(never()).setData(any(Uri.class));
    then(intent).should(never()).addCategory(anyString());
    then(intent).should(never()).setClassName(anyString(), anyString());
}
これでだいぶよくなったのではないでしょうか.改造を続けましょう
public class AppUtilsTest {
    @Mock
    private Intent mIntent;

    @Mock
    private Context mContext;

    @Mock
    private AppUtils.StartAppParam mParam;

    @Before
    public void setUp() throws Exception {
        whenNew(Intent.class).withArguments("package").thenReturn(mIntent);
    }

    @Test
    @PrepareForTest({AppUtils.class})
    public void testOpenAppByAction() {
        given(mParam.getAction()).willReturn("package");

        //when
        assertTrue(AppUtils.startApp(mContext, mParam));

        then(mContext).should().startActivity(mIntent);
        then(mIntent).should(never()).setData(any(Uri.class));
        then(mIntent).should(never()).addCategory(anyString());
        then(mIntent).should(never()).setClassName(anyString(), anyString());
    }
}
Intent、Context、AppUtilsのためです.StartAppParamは、異なるテストケースでよく使用する必要があります.メンバー変数として作成し、@Mockで自動mockを実現し、Mockitoを省きます.mock()メソッドの呼び出し.
次に、@Beforeで注記されたsetUp()メソッドにwhenNewメソッドを配置します.
今testOpenAppByActionを見るとずっと簡潔ではありませんか?コードの基礎が少しあれば、この例が検証に使われているのか、簡単に見ることができます.
もちろん、ここのBDDの書き方は上のCatchの書き方と比べると自然言語のように少し違います.
テスト用例を書き始めたら、このテスト用例を通過させるコードを書くことができます.このように動作テスト用例を先に書いてからコードを書く開発方式をBDDと呼ぶ.
mock静的方法
私たちが次に実現しなければならない機能は何ですか?パッケージ名でアプリケーションを起動しましょう.パッケージ名のみが設定されますが、Activity名は設定されていません.このアプリケーションのLaunch Activityを見つけてからアプリケーションを起動する必要があります.
AppUtilsでは、パッケージ名からActivity名を取得する方法を追加しました.
public class AppUtils {
    public static boolean startApp(Context context, StartAppParam param) {
        ...
    }
    
    public static String getLaunchActivityByPackage(Context context, String packageName) {
        return null;
    }
}
通常の開発プロセスであれば、getLaunchActivity ByPackageテストの例を書いて、この方法を実現する必要があります.ここではこのステップを省略してgetLaunchActivity ByPackageという方法を先に実現させずnullに戻り、テスト時に直接mockすればいいのです.
その後startAppByPackageのテスト例を書きます.
@Test
public void startAppByPackage() {
    mockStatic(AppUtils.class);

    given(AppUtils.startApp(any(Context.class), any(AppUtils.StartAppParam.class)))
            .willCallRealMethod();
    given(AppUtils.getLaunchActivityByPackage(any(Context.class), anyString()))
            .willReturn("LauncActivity");
    given(mParam.getPackageName()).willReturn("packageName");

    //when
    assertTrue(AppUtils.startApp(mContext, mParam));

    //then
    verifyStatic(); //  static     ,        AppUtils.getLaunchActivityByPackage     
    AppUtils.getLaunchActivityByPackage(any(Context.class), eq("packageName"));
    then(mIntent).should().setClassName(mParam.getPackageName(), "LauncActivity");
    then(mContext).should().startActivity(mIntent);
}
まず、mockStaticを使用してAppUtilsをシミュレートし、AppUtilsを構成します.startAppは実際のメソッドを呼び出し、getLaunchActivity ByPackageは直接「LauncActivity」を返します.
getLaunchActivity ByPackageが呼び出されていることを確認するときはverifyStatic()を呼び出します.
その後、AppUtilsが呼び出されたかどうかを次のように検証する.getLaunchActivity ByPackageそして「packageName」が入ってきました
AppUtils.getLaunchActivityByPackage(any(Context.class), eq("packageName"));
ここでは、getLaunchActivity ByPackageがprivateの方法であると仮定し、mockを以下の方法で行うことができます.
when(AppUtils.class, "getLaunchActivityByPackage", any(Context.class), anyString())
        .thenReturn("LauncActivity");
完全なDemo
他の残りのテストケースは一つ一つお話ししませんが、基本的にはこれまでのPowerMockの使い方の紹介で皆さんも自分で実現できるはずです.
完全なdemoコードはここから入手できます