言いたいことはそれだけか

JavaとかAndroidとか調べたことをメモします。٩( 'ω' )و

Dexopenerを使ってKotlinで書かれたfinalなclassをmockする話

[2017.7.27 追記あり]

4ヶ月以上前の話の続きです。下書きにずっと眠っていたのですが、諸事情により公開が遅くなってしまった…

こちらの記事でAndroidでKotlinのmockつらい〜〜〜 😇😇😇😇って話をしたら、

muumuutech.hatenablog.com

id:tmurakami さんがコメントでDexOpenerを教えてくださったので試してみました。 (id:tmurakamiさんがDexOpenerの作者さんです。)

github.com

使い方

上記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して読むことをおすすめします。 読み間違いなどあったらすみません!ご指摘いただけると嬉しいです!

github.com

ざっくり概要

[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.dataDircode_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以上という方はぜひ使ってみたらいいと思います!🎉🎉