Robolectricのquick startは、本家が詳しい。
build.gradleに、以下が必要(Android Studio 2系の場合)。1 2 3 4 5 6 7
testCompile 'org.robolectric:robolectric:3.4.2' testCompile 'org.robolectric:shadows-multidex:3.3.2' testCompile 'org.powermock:powermock-module-junit4:1.7.3' testCompile 'org.powermock:powermock-module-junit4-rule:1.7.3' testCompile 'org.powermock:powermock-api-mockito2:1.7.3' testCompile 'org.powermock:powermock-reflect:1.7.3' testCompile 'org.powermock:powermock-classloading-xstream:1.7.3'- Test Classは、Android Studioで開いた実class java fileの
public class CLASS名のところで黄色いヒントをclickしてCreate Test classを選択、JUnit4で作成 - 既存test classの
Whiteboxはorg.powermock.reflect.Whiteboxで置き換え - 既存test classの
@RunWithのMockitoJUnitRunnerはorg.mockito.junit.MockitoJUnitRunnerで置き換え org.mockito.exceptions.misusing.UnnecessaryStubbingException:というwarningが出るようになったので@RunWith(MockitoJUnitRunner.Silent.class)にすると解消
cf. How to resolve Unneccessary Stubbing exceptionRobolectricなTestは、1.@RunWith(RobolectricTestRunner.class)2.activity = Robolectric.setupActivity(SomeActivity.class);でActivityを起動RuntimeException: Multi dex installation failedと言われるのでshadows-multidexが必要
cf. Robolectric と Multidex でテストが落ちる問題の対応- static methodのmockはPowerMockと。
@RunWithがかぶっちゃうよ、どうしよう! → 本家に解説あり。要は、@PowerMockIgnoreでmockito、robolectric、android標準classesを除外、@PrepareForTestでstatic methodを持つclassを指定し、@Ruleを入れ(使わないのによくわからないが必要)、PowerMockito.mockStatic(...)で当該classを指定 NoClassDefFoundError: org/powermock/classloading/ClassloaderExecutorと言われるので、powermock-classloading-xstreamが必要
cf. version 1.5.5 java.lang.ClassNotFoundException: org.powermock.classloading.DeepCloner #597NoClassDefFoundError: org/mockito/cglib/proxy/MethodInterceptorと言われるので、powermock-api-mockito2と2でないとならない
cf. Problem with org.mockito.plugins.MockMaker and loading MethodInterceptor #819javax.xml.parsers.FactoryConfigurationError: Provider ...DocumentBuilderFactoryImpl cannot be cast to javax.xml.parsers.DocumentBuilderFactoryと言われるので@PowerMockIgnoreに"javax.xml.*", "org.xml.sax.*", "org.w3c.dom.*", "org.apache.log4j.*"が必要
cf. Powermock + Mockito + Spring = DocumentBuilderFactoryImplorg/powermock/default.properties is found in 2 placesと言われてerrorにはならないけどwarningが出るので、@PowerMockIgnoreに"org.powermock.*"も入れておく(試行錯誤の末なので参照なし)AsyncTaskがあっても、特段その終了を待たずにtestが終了してしまう。Robolectric.getBackgroundThreadScheduler().pause();でAsyncTask#doInBackground()を止める必要がある(AsyncTask#onPreExecute()は実行される)。ShadowAsyncTaskTest.javaを見ると、setUp()でRobolectric.getBackgroundThreadScheduler().pause();(とRobolectric.getForegroundThreadScheduler().pause();?)でthread止めて、asyncTask.execute()するとonPreExecute()が動き、次にShadowApplication.runBackgroundTasks();するとdoInBackground()、ShadowLooper.runUiThreadTasks();するとonPostExecute()が動く(ようだが、試してみるとShadowApplication.runBackgroundTasks()で返ってこなくなった。何故?!←これは単にAsyncTask中でdialog出して止まっていたため)- target class内でnewしているもののmockは、
PowerMockito.whennew(XXX.class).thenReturn(mock);だと、と言われて失敗する。1 2 3 4
org.mockito.exceptions.base.MockitoException: ClassCastException occurred while creating the mockito mock : ... You might experience classloading issues, please ask the mockito mailing-list. - shadow classでもstatic methodのmockが出来る。PowerMock使わずとも良い様子。いちいちShadow class作って各method毎に
@Implements書くのは面倒ではあるが、PowerMockを@Ruleして並存させると上述のようにclass loaderがどうのと言われて失敗したので、Robolectric一本で頑張った方がよさ気。PowerMock使わないならtestCompileもrobolectricとshadows-multidexの2つで済むし、PowerMock導入に伴って変更したMockito部分も変更不要になる。 - Custom Shadow classesの追加でcustom TestRunnerは作成不要、単に
@Configにshadows={ShadowXXX.class}と追記していけば良い。 - Shadowについて。Android APIのclassesについては、全て
ShadowXXXというclassが揃っている(e.g.ShadowActivity)。まるっとmockしたものを返したい場合には、custom shadow methodでreturn Shadow.newInstanceOf(ShadowBluetoothDevice.class);で良い。 - constructorもshadow出来る。constructorの場合には単に
public void __constructor__(...){...}でよく、@Implementationannotationは不要(あっても害はない)。 extendsしてるclassのconstructorの場合には、super classのconstructorのshadowingも必要。さもなくばsuper classの当該constructorが実行されてしまう。また、super classのconstructorもshadowingする場合、当該Shadow classの方もextendsしないとClassCastExceptionに見舞われる。A extends BでAのconstructorをshadowingしたらBのconstructorもshadowingし、ShadowA extends ShadowBにする必要がある。Shadows.shadowOf(myDialog).hasBeenDissmissed()といったようにUIの状態を取得できる。context.getPackageManager().getLaunchIntentForPackage("package name")がRobolectricsでやるとnullを返しやがるのでヌルポで失敗しくさる。多くの人が困っている模様。cf. PackageManager#getLaunchIntentForPackage() returns null #747 ←これによると2.2の頃から。3.4からPackageManager周りはRobolectricPackageManagerがdeprecatedになって他と同じようにShadowPackageManagerを使えとMigrating from 3.3 to 3.4にはあるが、shadowOf(RuntimeEnvironment.application.getPackageManager());としても、versionを3.3に落としてRuntimeEnvironment.setRobolectricPackageManager(packageManager);としても、testにおけるApplicationPackageManager#getLaunchIntentForPackageはnullを返す。仕方なく、ShadowApplicationPackageManagerをextendsしてcustom PackageManagerを作ってみても、何を@implementsしたらいいのか。PackageManager.classでは効かないし(抽象クラス?なのでそれは仕方ないのだろう)、android.app.ApplicationPackageManager.classでは何故か名前解決に失敗してcompile出来ない。RobolectricPackageManagerTest.javaを見ると、notNullValue()でassert出来そうなのだが、うまく行かなかった(Step Overしてやってみても、そもそもShadowApplicationPackageManagerではなくandroid.app.ApplicationPackageManager#getLaunchIntentForPackageが何かにmethod callを取られて空で返している感じ)。色々探して結局諦めた。ホントは、↓というようにやりたかったのだが。1 2 3
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick(); assertThat(shadowOf(activity).getNextStartedActivity().getAction(), is("jp.ideacross.allcardia.main.SplashActivity"));
Robospock -> ElectricSpock or Spock for Android
せめてresourceの場所なりと。
- RoboSpockですがちょっと更新が鈍いということでElectricSpock。但し新しい分情報少なし
- Spock for Androidもあり
- どちらも、directory structureがstandardでないとならない様子(要するに
app/src/main/java/...にsourceがありapp/src/test/groovy/...にSpock Testcodeがある)。build.gradleでのandroid.sourceSets.test.setRoot(...)は効かないようだった - 要は、
buildscript.dependenciesでclasspath 'org.codehaus.groovy:groovy-android-gradle-plugin:1.2.0'を指定、apply plugin: 'com.android.application'とapply plugin: 'groovyx.android'を指定、dependenciesにtestCompile 'org.spockframework:spock-core:1.0-groovy-2.4'を指定すれば素のSpock、testCompile 'com.github.hkhc:electricspock:0.6'ならElectricSpock、androidTestCompile 'com.andrewreitz:spock-android:2.0'ならSpock for Android(←これだけandroidTestCompileなのに注意)
という感じでしょうか。
Robolectric3 + RxJava(RxAndroid)1 + Retrofit2
RxJava + Retrofitなんて鉄板だからRobolectricによるtestなんてすぐ見つかると思ってたんですが、意外に手こずりました。要は、
- Retrofit2に対してはMockWebServer(OkHttp3 の MockWebServer を使う)
- RxJavaに対しては
RxJavaHooks(RxJava のテスト(2): RxJavaHooks, RxAndroidPlugins) MockWebServerは、例にあるように基本newしてMockResponseをenqueueしてurl(...)すればstartしてreturn valueにURL(http://localhost:XXXXX/←random port number)が入っているのでそれをRetrofitに食わせればいいのだけれども、URLをsetする部分はShadowの中なので、test classから直接食わせられず。なので固定port番号を使いたく、その場合server.url("/...");は不要で、でおk1server.start(portNumber);- ↑
httpになるとisCleartextTrafficPermitted()まわりで失敗するようになった。これは、isCleartextTrafficPermitted()fails on OpenJDK 8 + Robolectric #2533にあるように、NetworkSecurityPolicyをShadowしてやればよい。 - RxJavaの
onNextやonCompletedが実行されない問題は、Robolectric.flushBackgroundThreadScheduler();ではなく、によって別threadじゃなくmain threadで実行するようにすればおk1RxJavaHooks.setOnNewThreadScheduler(s -> Schedulers.immediate()); - 上記の話は、
Retrofit2のService interfaceでObservable<...>を返す場合のもの。Call<...>を返す形にしてenqueue()してCallback<...>でonResponse()、onFailure()でhandleする場合には、こうは行かなかった(onResponse()もonFailure()も実行されない)。ShadowLooper.runUiThreadTasks()でうまく行くようなことを書いてある情報(Testing retrofit 2 with robolectric, callbacks not being called)もあったが、症状変わらず。OkHttpのMockWebServerとRobolectricでFragmentの動作をテストするにRetrofit2内で使っているOkHttpClient.Builder#newBuilderをshadowしてうまく行く話があったので、試すと確かにonResponse()が呼ばれた! ただ、今回ぼくは実classの方でnew Retrofit().newBuilder().client(new OkHttpClient().newBuilder().build())とかってclientmethodを使っておらずdefaultで裏でimplicitlyに生成されるOkHttpClientそのまま使っており、それだとnewBuilder()呼ばれないので、色々辿ってった挙句、okhttp3.Dispatcher#executorServiceをshadowして、前述のpageにあったようにすぐcommand.run()するexecutemethodを持つAbstractExecutorServiceclassを返してやると、うまく行った。Dispatcher#executorServiceってjava.util.concurrent.ThreadPoolExecutorをdefaultでは使っており、Androidのthreadとは違うから、uncontrollableだったんですね。考えてみるに、RxAndroidと違いRetrofitはAndroid専用ではないので、java.util.concurrentのExecutor使ってるのも当然ですか。
AccountManager with Robolectric(というかMockito)
基本的には、
AccountManager.get(Context)はJUnit Test内でもtarget class内でも同じobjectを返すので、そのままassertion可能ただ、例えば
manager.blockingGetAuthToken(...)でExceptionを起こさせたい時は、AccountManager manager = spy(AccountManager.get(application));したmanagerをgetSystemService(Context.ACCOUNT_SERVICE)でdoReturnするようにしたApplicationをspyして、そのapplicationをRuntimeEnvironment.applicationの代わりにねじ込む必要がある(Using mockito to mock AccountManager)。具体的には、1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@Rule public ExpectedException thrown = ExpectedException.none(); : Account account = new Account("any name", CarCloudAuthUtil.ACCOUNT_TYPE); Application application = spy(RuntimeEnvironment.application); util = new CarCloudAuthUtil(application); AccountManager manager = spy(AccountManager.get(application)); doReturn(manager) .when(application) .getSystemService(Context.ACCOUNT_SERVICE); manager.addAccountExplicitly(account, "any key", new Bundle()); manager.setAuthToken(account, CarCloudAuthUtil.AUTH_TOKEN_TYPE, "any string"); doThrow(AuthenticationException.class) .when(manager) .blockingGetAuthToken(eq(account), eq(CarCloudAuthUtil.AUTH_TOKEN_TYPE), eq(true)); thrown.expect(AuthenticationException.class); thrown.expectMessage(new IsNull());Exceptionのassertionは、@Test(expected=...)でも良いが、@Ruleでも書ける(JUnitでの例外テストの書き方)。その場合、Exception#messageがnullの場合のassertionはorg.hamcrest.core.IsNullを用いてthrown.expectMessage(new IsNull());とする(ExpectedException.expectMessage((String) null) is not working)。mocking method実行時に他のことをしたい時には、
when(mock.methodCall()).thenAnswer(m -> {...});とlambdaで書ける(mockitoとJMockitについてのメモ)。