Dexopenerを使ってKotlinで書かれたfinalなclassをmockする話
[2017.7.27 追記あり]
4ヶ月以上前の話の続きです。下書きにずっと眠っていたのですが、諸事情により公開が遅くなってしまった…
こちらの記事でAndroidでKotlinのmockつらい〜〜〜 😇😇😇😇って話をしたら、
id:tmurakami さんがコメントでDexOpenerを教えてくださったので試してみました。 (id:tmurakamiさんがDexOpenerの作者さんです。)
使い方
上記RepositoryのREADMEにある通り、testコードのcompile時に対象libraryを指定して、 testInstrumentationRunner
を指定するだけです。
dependencies { androidTestCompile 'com.github.tmurakami:dexopener:0.9.1' }
android { defaultConfig { testInstrumentationRunner "com.github.tmurakami.dexopener.DexOpenerAndroidJUnitRunner" }
これだけでfinal classをmockできるようになります!楽!!
中で何が起こっているのか
せっかくなのでlibraryの中身を読んでみました。Open source最高!!
こちらのlibraryは同じ作者さんの classinjector
というlibraryに依存を持っているのでコードを読むときは手元にcloneして読むことをおすすめします。
読み間違いなどあったらすみません!ご指摘いただけると嬉しいです!
ざっくり概要
[2017.7.27 追記]
最新version( 0.10.3
)で方針が変わってかなり早くなりました!圧倒的…!
ただし、minSdkVersionが16にあがっていましたのでご注意を。
version | time |
---|---|
0.9.1 | Total time: 3 mins 37.83 secs |
0.10.3 | Total time: 9.162 secs |
dexファイルを取って来て、下記のクラス・メソッドのアクセス修飾子からfinalをマスクしてbit反転していました。
- class
- inner class
- method
というわけでクラスやメソッドが増えれば増えるほど処理に時間がかかります。
コード詳細
まずはTest Runner ( DexOpenerAndroidJUnitRunner.java
) のnewApplicationで DexOpener#install()
をコール。
このメソッドの中で ApplicationInfo
を持ったbuilderを生成し、 DexOpenerImpl
に処理を移譲します。
この時、Builderに BuiltinClassNameFilter
を設定して、buildinのclassはmockできないようにしています。
BuiltinClassNameFilter.java
private final String[] disallowedPackages = { "android.", "com.android.", "com.github.tmurakami.classinjector.", "com.github.tmurakami.dexmockito.", "com.github.tmurakami.dexopener.", "com.github.tmurakami.mockito4k.", "java.", "javax.", "junit.", "kotlin.", "kotlinx.", "net.bytebuddy.", "org.hamcrest.", "org.jacoco.", "org.junit.", "org.mockito.", "org.objenesis.", };
ApplicationInfo.dataDir
に code_cache/dexopener
でcacheを生成した後、
ClassInjector
を使ってApplicationInfo
からclasses*.dexファイルを取って来て、
AndroidClassSource.java
@SuppressWarnings("TryFinallyCanBeTryWithResources") private ClassSource newDelegate() throws IOException { List<ClassSource> sources = new ArrayList<>(); ClassNameReader r = new ClassNameReader(classNameFilter); ZipInputStream in = new ZipInputStream(new FileInputStream(sourceDir)); // sourceDirはApplicationInfoから取得したやつ try { for (ZipEntry e; (e = in.getNextEntry()) != null; ) { String name = e.getName(); if (name.startsWith("classes") && name.endsWith(".dex")) { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[16384]; for (int l; (l = in.read(buffer)) != -1; ) { out.write(buffer, 0, l); } ApplicationReader ar = new ApplicationReader(ASM4, out.toByteArray()); sources.add(dexClassSourceFactory.newClassSource(ar, r.readClassNames(ar))); } } } finally { in.close(); } return new ClassSources(sources); }
[2017.7.27 追記]
コメントいただきました通り、最新版ではASMDEXではなくdexlib2を使う方針に変わっていました。失礼しました。
下記のfinalをbit反転しています。
- class
- inner class
- method
visitorパターンで実装されていました。ASMDEXのClassVisitor を使っているためかな?
ApplicationOpener
@Override public ClassVisitor visitClass(int access, String name, String[] signature, String superName, String[] interfaces) { int acc = access & ~ACC_FINAL; return new ClassOpener(api, super.visitClass(acc, name, signature, superName, interfaces)); }
感想
本体側をKotlinで書かれているアプリのinstrumented auto testしたい〜〜って時に選択肢の一つとして良さそうです🎉🎉
[2017.7.27 追記]
パフォーマンスもかなり良いので、minSdkVersionが16以上という方はぜひ使ってみたらいいと思います!🎉🎉