u-ryo's blog

various information for coding...

Category: Android

Robolectric + PowerMock

| Comments

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のWhiteboxorg.powermock.reflect.Whiteboxで置き換え
  • 既存test classの@RunWithMockitoJUnitRunnerorg.mockito.junit.MockitoJUnitRunnerで置き換え
  • org.mockito.exceptions.misusing.UnnecessaryStubbingException:というwarningが出るようになったので@RunWith(MockitoJUnitRunner.Silent.class)にすると解消
    cf. How to resolve Unneccessary Stubbing exception
  • Robolectricな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 #597
  • NoClassDefFoundError: org/mockito/cglib/proxy/MethodInterceptorと言われるので、powermock-api-mockito22でないとならない
    cf. Problem with org.mockito.plugins.MockMaker and loading MethodInterceptor #819
  • javax.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 = DocumentBuilderFactoryImpl
  • org/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使わないならtestCompilerobolectricshadows-multidexの2つで済むし、PowerMock導入に伴って変更したMockito部分も変更不要になる。
  • Custom Shadow classesの追加でcustom TestRunnerは作成不要、単に@Configshadows={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__(...){...}でよく、@Implementation annotationは不要(あっても害はない)。
  • extendsしてるclassのconstructorの場合には、super classのconstructorのshadowingも必要。さもなくばsuper classの当該constructorが実行されてしまう。また、super classのconstructorもshadowingする場合、当該Shadow classの方もextendsしないとClassCastExceptionに見舞われる。A extends BAの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#getLaunchIntentForPackagenullを返す。仕方なく、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.dependenciesclasspath 'org.codehaus.groovy:groovy-android-gradle-plugin:1.2.0'を指定、apply plugin: 'com.android.application'apply plugin: 'groovyx.android'を指定、dependenciestestCompile '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してMockResponseenqueueしてurl(...)すればstartしてreturn valueにURL(http://localhost:XXXXX/←random port number)が入っているのでそれをRetrofitに食わせればいいのだけれども、URLをsetする部分はShadowの中なので、test classから直接食わせられず。なので固定port番号を使いたく、その場合server.url("/...");は不要で、
    1
    
    server.start(portNumber);
    
    でおk
  • httpになるとisCleartextTrafficPermitted()まわりで失敗するようになった。これは、isCleartextTrafficPermitted() fails on OpenJDK 8 + Robolectric #2533にあるように、NetworkSecurityPolicyをShadowしてやればよい。
  • RxJavaのonNextonCompletedが実行されない問題は、Robolectric.flushBackgroundThreadScheduler();ではなく、
    1
    
    RxJavaHooks.setOnNewThreadScheduler(s -> Schedulers.immediate());
    
    によって別threadじゃなくmain threadで実行するようにすればおk
  • 上記の話は、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.concurrentExecutor使ってるのも当然ですか。

AccountManager with Robolectric(というかMockito)

  • 基本的には、AccountManager.get(Context)はJUnit Test内でもtarget class内でも同じobjectを返すので、そのままassertion可能

  • ただ、例えばmanager.blockingGetAuthToken(...)でExceptionを起こさせたい時は、AccountManager manager = spy(AccountManager.get(application));したmanagergetSystemService(Context.ACCOUNT_SERVICE)doReturnするようにしたApplicationspyして、そのapplicationRuntimeEnvironment.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#messagenullの場合の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についてのメモ)。

Make SqlCipher Faster

| Comments

他に言及しているsourceが全く無かったのでまさかと思っていたのですが、keyの形式を変えて爆速には本当でした。 試しに、Allcaridaからc.binだけ持ってきてAndroid sample applicationを作って計測した所、従来のkey(4文字)だと約0.4秒、上記ページ例の64字だと0.02秒と顕著な差がありました。

従いまして、「SqlCipherのkeyを64文字の16進数にすれば速くなる」が結論です。

1
2
3
4
5
6
7
従来
09-27 16:25:59.010 30173-30173/sqlcipher.test.jmtech.co.jp.sqlciphertest D/Open: /data/user/0/sqlcipher.test.jmtech.co.jp.sqlciphertest/databases/c.bin
09-27 16:25:59.400 30173-30173/sqlcipher.test.jmtech.co.jp.sqlciphertest D/After open: /data/user/0/sqlcipher.test.jmtech.co.jp.sqlciphertest/databases/c.bin

64字key
09-27 16:24:26.060 28517-28517/sqlcipher.test.jmtech.co.jp.sqlciphertest D/Open: /data/user/0/sqlcipher.test.jmtech.co.jp.sqlciphertest/databases/c2.bin
09-27 16:24:26.080 28517-28517/sqlcipher.test.jmtech.co.jp.sqlciphertest D/After open: /data/user/0/sqlcipher.test.jmtech.co.jp.sqlciphertest/databases/c2.bin

そこで、実際にAllcaridaでc.bin,r.binだけ64字key版を作って「履歴一覧」画面表示を比較してみると、従来約3.5秒のところ約0.2秒で開けることを確認しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
従来 3.46秒(約0.6秒×5+α)
09-27 17:53:03.670 9054-9054/jp.ideacross.allcardia.main D/After: <<HistoryMainActivity#activityStart:49>> 履歴一覧
09-27 17:53:03.680 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> c.bin Open
09-27 17:53:04.360 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:53:04.380 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r.bin Open
09-27 17:53:05.010 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:53:05.050 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> c.bin Open
09-27 17:53:05.710 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:53:05.720 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r.bin Open
09-27 17:53:06.370 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:53:06.390 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r.bin Open
09-27 17:53:07.030 9054-10071/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open

64字key 0.21秒(0.0+0.01+0.01+0.02+0.03+α)
09-27 17:50:57.350 4493-4493/jp.ideacross.allcardia.main D/After: <<HistoryMainActivity#activityStart:49>> 履歴一覧
09-27 17:50:57.360 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> c2.bin Open
09-27 17:50:57.360 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:50:57.400 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r2.bin Open
09-27 17:50:57.410 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:50:57.460 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> c2.bin Open
09-27 17:50:57.470 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:50:57.480 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r2.bin Open
09-27 17:50:57.500 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open
09-27 17:50:57.530 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:964>> r2.bin Open
09-27 17:50:57.560 4493-5754/jp.ideacross.allcardia.main D/After: <<DatabaseHelper#open:970>> After Database Open

More concretely,

SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(databaseFile, "x\'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99\'", null);

in Java.

To get a rekeyed file,

1
2
3
4
5
6
7
8
9
10
11
$ sudo apt install sqlcipher
$ sqlcipher /tmp/c.bin
SQLCipher version 3.15.2 2016-11-28 19:13:37
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> PRAGMA key = '7824';
sqlite> ATTACH DATABASE 'c2.bin' AS c KEY "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'";
sqlite> SELECT sqlcipher_export('c');

sqlite> DETACH DATABASE c;
sqlite>

You'll get c2.bin with the new 64bit key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sqlcipher r.bin
SQLCipher version 3.8.6 2014-08-15 11:46:33
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> PRAGMA key = "7824";
sqlite> ATTACH DATABASE 'r2.bin' AS r KEY "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'";
sqlite> SELECT sqlcipher_export('r');

sqlite> DETACH DATABASE r;sqlite> PRAGMA user_version;
0
sqlite> PRAGMA user_version = 6;
sqlite> PRAGMA user_version;
6
sqlite>

You'll get r2.bin with the new 64bit key and version 6.

PRAGMA user_version is needed because in SQLiteOpenHelper class judges whether it calls onCreate(table creation) by db.getVersion(). [Android]データベースをアップグレードする時

そもそも画面遷移に4秒も掛かるようなAndroidアプリをリリースするなんていうのもunbelievableですが、そういう人達なので...

まぁ、暗号化するにせよ自分ならRealm使うので、こんな知識不要ですけど、SqlCipher使うなら最初から64字16進code使うべきなんですね。

Test Failed on AndroidStudio

| Comments

Android Studio(2.3.1)で久し振りにtestを動かしてみると、

1
Caused by: java.lang.ClassNotFoundException: android.view.View$OnClickListener

と言われて動かなくなりました。 instrumentation testではなくフツーのtestですjunit4とmockitoの。 java.lang.NoClassDefFoundError:android and junit testを見付けて、えーとか思いつつもやってみたら、確かに直りました。

1
$ rm -rf .gradle

追記

projectをcleanした後、いくらbuildしても「databinding classが見つからない」と言われて困った時にも効きました。

Current Project I'm Working in

| Comments

今、グループ内会社のAndroidアプリ開発に売られてるんですけど、そこのソフトの作りがひどくてひどくて泣けてきます。

  1. 「結果が表示されなくなった」というので見てみたら、toString()が変わっていたのが原因。より根本的な原因は、toString()というdebug用途のmethodをoverrideしてmainのlogicに使っていること。まぁ、Activity跨ぐstructured dataをParcelableにする時間が無かった、という事情は分かるんですけど、StringBuilder#toStringですら使わずnew String(StringBuilder)とするくらいなのでぼくは。
  2. 「途中で落ちる」というので見てみると、ヌルポが出てました。どうしてかなー、と見ていくと、途中でnullを代入しているmethodが呼ばれています。どうしてこれを呼ぶよう変えたのか聞いてみると、終了処理をちゃんとするようしてる時に、comment outしてあったこのfinishっぽいmethodをcomment inしたんだそう。それがどういう効果を持つのかわからぬまま、そうしたんだって。えーーーっ!?
  3. 極めつけは、今日わかったんですが、HashMapListにしてその0番目を使ってるんですね。えーーーっ! どうしてAndroid 4.4.2ではうまく動かないの? というのを探っていったら、そこに行き着きました。逆に、これまでよく動いていましたねぇ。素晴らしい!! 先月までいた派遣のフリー技術者が書いたcodeの一部でしたけど、わざと書いたならいざ知らず、もし意識せず書いたのなら、恐ろしいです。

Illegal Keysize

| Comments

AndroidでNanoHTTPDを 動かすprogramを開発しているんですが、 WebRTCにするのに、TLSが必要じゃないですか。 そのserver certを普通に作ると、Illegal keysizeと言われて key load時に落ちるのでハマりました。 NanoHTTPDの解説にあるように、

1
$ keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1  -validity 9999

と作ってもダメでした。 色々調べると、AndroidにはBouncyCastle(BKS)でないとならないらしく、 証明書は面倒なのでsnakeoilを流用して、

1
2
$ sudo openssl pkcs12 -export -in /etc/ssl/certs/ssl-cert-snakeoil.pem -inkey /etc/ssl/private/ssl-cert-snakeoil.key -out ~/AndroidStudioProjects/SharedEye/ssl-cert-snakeoil.p12 -name ssl-cert-snakeoil
$ /usr/lib/jvm/java-8-oracle/bin/keytool -importkeystore -deststorepass password -destkeypass password -destkeystore snakeoil.jks -srckeystore ssl-cert-snakeoil.p12 -srcstoretype PKCS12 -srcstorepass password -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-jdk15-1.46.jar

とすると、

1
Problem importing entry for alias java.security.KeyStoreException: java.io.IOException: Error initialising store of key store: java.security.InvalidKeyException: Illegal key size.

と言われてimport出来ませんでした(→jksが作れませんでした)。 かなり悩んだのですが、結局JavaでAES256を使用できるようにするにあるように、 JCE(Java Cryptography Extension)を落としてきて local_policy.jarを上書きしたら、 jksも出来て、Android側でも何事もなくloadしてくれました。