Maven の Surefire プラグインと JMockit と JaCoCo プラグイン
ビルドツールとして Maven を使っている Java プロジェクトで JMockit と JaCoCo を使いたいときの話。 ユニットテストの実行には Maven Surefire プラグインを使用しているものとする。 また、JaCoCo の適用には JaCoCo Maven プラグインを使用するものとする。
JMockit と JaCoCo の -javaagent
指定を共存させる
JMockit も JaCoCo も、Java プログラミング言語エージェントの仕組みを利用している *1。 なので、どちらも基本的には -javaagent
の指定が必要である。
JMockit の -javaagent
指定
JMockit の方は、Surefire プラグインの argLine
パラメータで -javaagent
を指定する。
<plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.21.0</version> <!-- or some other version --> <configuration> <argLine> -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar </argLine> <!-- ... --> </configuration> <plugin> <!-- ... --> <plugins>JMockit - Tutorial - Introduction
JaCoCo の -javaagent
指定
JaCoCo については jacoco:prepare-agent
ゴールにて argLine
プロパティ *2 に -javaagent
が追加される仕組みになっている。
Surefire プラグインに argLine
パラメータが指定されていないときには argLine
プロパティの値が使われるので、Surefire プラグインに argLine
パラメータが指定されていない場合にはそれ以上の設定は必要ない。
一方で、Surefire プラグインに argLine
パラメータの指定がされている場合は (そのままでは) JaCoCo プラグインが設定した argLine
プロパティの値が使われないため、別途設定が必要となる。 具体的には、Surefire プラグインの late property evaluation (late replacement) を使用する。 これは、プロパティ参照の ${...}
の代わりに @{...}
を使うというもの。 ${...}
を使った場合はプラグインの実行前に評価されるため JaCoCo プラグインが argLine
プロパティを変更してもそれが反映されないが、@{...}
を使うとプラグインの実行後の値が反映される。
One of the ways to do this in case of maven-surefire-plugin - is to use syntax for late property evaluation:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>@{argLine} -your -extra -arguments</argLine> </configuration> </plugin>
IntelliJ IDEA での問題
さて、Maven でビルドを実行する際に JMockit と JaCoCo を共存させるだけならここまでで解決するが、さらにややこしいのが IntelliJ IDEA の存在である。
IntelliJ IDEA で Maven プロジェクトを扱う場合、デフォルトでは JUnit 実行時に Surefire プラグインの argLine
パラメータを読み取るのだが、IntelliJ IDEA は late replacement (@{...}
) に対応していない。 そのため、下記のようなエラーが発生してしまう。
エラー: メイン・クラス@{argLine}が見つからなかったかロードできませんでした
- IntelliJ IDEA が
@{...}
を解釈しない件 : https://youtrack.jetbrains.com/issue/IDEA-143187
回避策の一つは、IntelliJ IDEA でのユニットテスト実行時に Surefire プラグインの argLine
パラメータを参照する設定を無効にすること。 設定の中の 「Maven」 の 「Running Tests」 で Surefire プラグインの argLine
を参照するかどうか指定できる。
私が採った回避策は、argLine
パラメータには空文字列として宣言したプロパティを含めておき、JaCoCo を有効にしたいときだけコマンドライン引数で @{argLine}
を埋め込むというもの。
<properties> <!-- Surefire プラグインの `argLine` に含める文字列。 JaCoCo の Java Agent を有効にするためには `@{argLine}` を指定すること。 IntelliJ IDEA が `@{...}` による late replacement に対応しておらず、直接 `@{argLine}` を指定すると IntelliJ IDEA でのテスト実行時にエラーが発生してしまうので、`@{argLine}` をコマンドラインオプションで指定できるように このようなプロパティを用意した。 See : http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html (Surefire Plugin の `argLine` の説明) See : https://www.eclemma.org/jacoco/trunk/doc/prepare-agent-mojo.html (JaCoCo の Java Agent の指定について) See : https://youtrack.jetbrains.com/issue/IDEA-143187 (IntelliJ IDEA が argLine の `@{...}` を解釈しない件) --> <surefireArgLine/> </properties> <!-- (中略) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <argLine> ${surefireArgLine} <!-- See : http://jmockit.github.io/tutorial/Introduction.html#runningTests --> -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar </argLine>
上のようにしておけば IntelliJ IDEA でのテスト実行にエラーが出ることはない。 そして Maven をコマンドラインから動かすときには、下記のように指定すれば JaCoCo が有効になる。
mvn package -D surefireArgLine="@{argLine}"
もっといい方法があれば知りたい……。
参考
- Java プログラミング言語エージェントについて : java.lang.instrument (Java SE 10 & JDK 10 )
Android アプリバンドル (Android App Bundle) について学んだ
2018 年の Google I/O でも発表があった *1 Android アプリバンドル (Android App Bundle)。 Android Studio 3.2 を使っているとアプリのビルドで App Bundle を選べるようになっていたりするし何となく存在は知っていたけどちゃんと調べてはいなかった。
Instant アプリを試しに作ってみようとしたら Android アプリバンドルの知識が必要になったので、この機会にちゃんと調べてみることにした。 とりあえずコードラボの内容をさーっと見たのでまとめておく。
- コードラボ : Your First Android App Bundle
Android アプリバンドルについて
- Android アプリバンドル (Android App Bundle) : Google Play へアップロードするための新しい形式。 アプリのコンパイルされたコードとリソースをすべて含むが、APK 生成や署名は Google Play に任される。
- 動的配信 (Dynamic Delivery) : Google Play における新しいアプリ提供のモデル。 各ユーザー端末に最適化された APK を生成して提供するためにアプリバンドルが使用される *2。
- 動的機能モジュール (dynamic feature modules) : この種のモジュールをアプリのプロジェクトに追加し、アプリバンドルに含めることで、必要になってから動的配信を通じてダウンロードされるアプリの機能を作ることができる。 2018 年 11 月 11 日時点ではまだベータ版。
動的配信 (Dynamic Delivery) は、分割 APK 機構 (split APK mechanism) *3 を基礎としている。 分割 APK により、Google Play は大きなアプリを小さなパッケージに分解できる。 分割 APK の種類は 3 種類。
- Base APK : 他の分割 APK から使用されるコードやリソースを含み、アプリの基本機能を提供する。 ユーザーがアプリをダウンロードする際には常にこの APK が含まれる。
- Configuration APKs : 指定された端末構成のためのネイティブライブラリとリソースのみを含む。 ロケールや画面密度、CPU アーキテクチャといったものに依存する APK コンテンツを最適化するため。
- Dynamic feature APKs : アプリが最初にインストールされたときには必要でないが、あとからダウンロードされて使われるかもしれない機能を含む。
これらの APK は Google Play がビルドして提供してくれる。 Android 4.4 (API level 20) 以下の端末向けには、Google Play が自動で端末構成に最適化された単一 APK を提供してくれる。
Android Studio 3.2 でアプリバンドルを生成する
プロジェクト自体は普通に作成すればそれで良い。 (Dynamic feature module を作らないなら、プロジェクト構造は通常の app ディレクトリ下にアプリ全体のコードやリソースを含める形式で良い。)
Android Studio で、「Build」 メニューから 「Build Bundle(s)」 を選んだり、「Generate Signed Bundle or APK」 の 「Android App Bundle」 を選んだりすることでアプリバンドルを生成できる。
コードラボには、app/build.gradle の android { }
ブロックに下記の内容を追加する必要があるようなことが書かれているが、「Add support for Dynamic Delivery | Android Developers」 を見る限りは Android Studio 3.2 の正式版の段階ではデフォルトで有効になっているようで、下記記述はしなくても良さそう。
bundle { language { enableSplit = true } density { enableSplit = true } abi { enableSplit = true } }
アプリバンドルの確認
アプリバンドルを確認する方法は 2 つ。
- ローカルで bundletool コマンドラインツールを使用する。
- Play Console にバンドルをアップロードし、内部テストトラック (internal test track) を使って Google Play を通して確認。
Bundletool というのは、Gradle や Android Studio、Google Play が Android アプリバンドルをビルドしたり、アプリバンドルから種々の APK に変換したりするのに使用しているツール。 コマンドラインツールとしても使用できる。 「Releases · google/bundletool · GitHub」 で JAR 形式で配布されている。 使用方法はコードラボにも書かれているし、「bundletool | Android Developers」 などにも書かれている。
アプリバンドルを Google Play にアップロードする
Google Play へのアップロードで気を付ける必要があるのは 「Google Play アプリ署名」 を使用する必要があるということぐらいで、それ以外には特に難しいところはなかった。
App Bundle を使用するアプリには Google Play アプリ署名が必要です。アプリ署名を有効にした後の流れは次のとおりです。
アプリ署名鍵を管理する - Play Console ヘルプ
おまけ : アプリバンドルの instant app サポート
2017 年から instant apps というものは存在していた *4 が、アプリバンドルによってより簡単に実現できるようになった。
- Create your first instant app | Android Developers
- Add an instant experience to your existing Android App Bundle | Android Developers : アプリバンドルに instant アプリの機能を追加する方法
- Android Studio Release Updates: Android Studio 3.3 Canary 11 available : Android Studio 3.3 Canary 11 では Android Studio でのサポートも追加された模様。
ドキュメントを見る限りは簡単なのだけど、自分の場合は動作確認にかなりてこずってしまった。 また今度まとめる。
関連ページ
*1:Google Developers Japan: Google I/O 2018:Android の新機能 など参照。
*2:それぞれの端末に必要なコードやリソースだけがダウンロードされる。
*3:Android 5.0 (API level 21) 以降で使用可能
*4:【インストールせずにすぐ実行できる!】Android Instant Apps を使ってみた – PSYENCE:MEDIA など。
MockK と JMockit の組み合わせで AttachNotSupportedException 例外が発生することがあるっぽい
Kotlin で Mockito を使うのが辛くなってきた *1 ので、「よーしパパ MockK 入れちゃうぞー」 と言って MockK 1.8.12 を導入したのだけど、その結果テストを実行すると以下のような初期化エラーが発生するようになってしまった。
java.lang.ExceptionInInitializerError at MyTest.<init>(MyTest.kt:101) Caused by: java.lang.IllegalStateException: Error during attachment using: net.bytebuddy.agent.ByteBuddyAgent$AttachmentProvider$Compound@718207 at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:384) at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:358) at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:326) at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:312) at io.mockk.proxy.jvm.JvmMockKAgentFactory.initInstrumentation(JvmMockKAgentFactory.kt:122) at io.mockk.proxy.jvm.JvmMockKAgentFactory.init(JvmMockKAgentFactory.kt:31) at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:45) at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:163) ... 22 more Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at net.bytebuddy.agent.Attacher.install(Attacher.java:84) at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:379) ... 29 more Caused by: com.sun.tools.attach.AttachNotSupportedException: no providers installed at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208) ... 35 more
スタックトレースの一番下を見ると com.sun.tools.attach.VirtualMachine
なので、JDK の内部でなんかなってるのかなー、と最初は思っていたのだけど、IDE で辿っていくと VirtualMachine
クラスは JMockit に含まれているものだということがわかった。 使っていた JMockit が 1.22 と少し古いものだったので、最新の 1.43 を使うようにしたら無事エラーは発生しなくなった。
Mockito でも同様のエラーが発生することがある模様。
ちなみに com.sun.tools.attach.VirtualMachine
は、JVM にアタッチするための API に含まれるものらしい。
JaCoCo の Java 10 対応はバージョン 0.8.1 から
Gradle 4.6 で Gradle の JaCoCo プラグインを使用しているプロジェクトをビルドすると下記のエラーが発生した。 Java 環境は OpenJDK 10。
java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:564) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:510) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:522) Caused by: java.lang.RuntimeException: Class java/lang/UnknownError could not be instrumented. at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:139) at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:100) at org.jacoco.agent.rt.internal_290345e.PreMain.createRuntime(PreMain.java:55) at org.jacoco.agent.rt.internal_290345e.PreMain.premain(PreMain.java:47) ... 6 more Caused by: java.lang.NoSuchFieldException: $jacocoAccess at java.base/java.lang.Class.getField(Class.java:1958) at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:137) FATAL ERROR in native method: processing of -javaagent failed ... 9 more
『java.lang.NoSuchFieldException: $jacocoAccess』 と出ているので JaCoCo 関連っぽいということで調べたところ、JaCoCo の JDK 10 対応は JaCoCo 0.8.1 からだった。
- Support JDK 10 by Godin · Pull Request #629 · jacoco/jacoco · GitHub
- Release 0.8.1 · jacoco/jacoco · GitHub
Gradle 4.6 時点での JaCoCo プラグインでは、デフォルトで JaCoCo 0.8.0 を使うようになっているので、JDK 10 で使うには JaCoCo のバージョンを指定してやる必要がある。
jacoco {
toolVersion = '0.8.2'
}
- Gradle 4.6 時点での JaCoCo プラグインの設定 : JacocoPluginExtension - Gradle DSL Version 4.6
ちなみに Gradle 4.10.2 時点ではデフォルトは JaCoCo 0.8.1 になっている。
Azure Pipelines で Android アプリの CI をやってみてる
最近 Microsoft から発表された Azure DevOps。 Visual Studio Team Foundation (VSTF) をリブランドしたものだそう。
- Azure DevOps の概要 | ブログ | Microsoft Azure
- Visual Studio Team ServicesからAzure DevOpsへ - kkamegawa's weblog
VSTF のときに試しのプロジェクトを 1 個作ってそのまま放置していたのだけど、せっかくなのでこの機会に触ってみることにした。 まずは CI/CD サービスの Azure Pipelines を使ってみてる。
やってみた
単純なビルド
GitHub でホスティングしている Android プロジェクトの CI として、単に ./gradlew build
するぐらいのものを構成するだけならすごく簡単。
Azure DevOps で新しいプロジェクトを作成して、その中の 「Pipelines」 の 「Builds」 を開くと新しいビルドパイプラインの構成を行う画面が出てくる。 Web 上でぽちぽちするとビルドパイプラインの設定が作られてそのまま GitHub の pull request にしてくれた。
- Azure DevOps が作ってくれた pull request : Set up CI with Azure Pipelines by nobuoka · Pull Request #5 · nobuoka/android-Yuyu-Tumblr · GitHub
ただ、Gradle のビルドファイルだけを見て 「Gradle プロジェクトだ」 って判断されるみたいで、初期の構成は Android 向けではなくて普通の Gradle プロジェクト向けになってしまっている。 なので、「Build Android apps with Azure Pipelines or TFS - Azure Pipelines & TFS | Microsoft Docs」 を見ながら自分で構成をいじる必要がある。
pool: vmImage: 'macOS-10.13' steps: - task: Gradle@2 inputs: workingDirectory: '' gradleWrapperFile: 'gradlew' gradleOptions: '-Xmx3072m' publishJUnitResults: true testResultsFiles: '**/TEST-*.xml' tasks: 'build'
『The Android Emulator is currently available only on the Hosted macOS agent.』 とのことで、主には VM image を MacOS にする必要がある *1。
自動ビルドの設定
私の場合、初期状態だと GitHub からの web hook が無効になっていた *2。 ビルドパイプラインの設定 (YAML ファイルじゃなくて web 側) の中に 「Triggers」 ってのがあるので、そこで web hook を有効にするとコミットごとなどの CI 実行を設定できる。
テストカバレッジの Codecov へのアップロード
Codecov にテストカバレッジをアップロードするにはコマンドを実行する必要がある。 Bash タスクも用意されているので、簡単に実現できる。
steps: - task: Gradle@2 (中略) - task: Bash@3 inputs: targetType: 'inline' script: 'bash <(curl -s https://codecov.io/bash)' workingDirectory: '' env: 'CODECOV_TOKEN': '$(codecovToken)'
codecovToken
は自分で設定したビルドパイプラインの secret variable で、環境変数として使うにはこのように明示的にマッピングしてやる必要がある。 最初 env
プロパティの部分を inputs
の中に入れてしまっていて、YAML パースエラーが発生してちょっとはまってた。
Azure Pipelines の bash タスクで環境変数を設定しようとしてるのだけど 『azure-pipelines.yml (Line: 21, Col: 7): Expected a scalar value』 って怒られて原因不明で難しい。 https://t.co/6pwYiQi2Iy
— Nobuoka Yu (@nobuoka) 2018年10月27日
雑感
まだビルド実行とカバレッジのアップロードぐらいしかやっていないけど、それぐらいなら (ちょっとハマった箇所もあったけど) 素直に構成できた。 ビルドパイプライン単体で他の CI/CD ツールと比較しての強みはまだ見えてないのだけど、Azure DevOps として他のサービスとの連携がやりやすい点は強みっぽい。
ライブラリなどのキャッシュ周りとか、ビルド環境を準備するのが大変な場合にどうするのか (CircleCI では Docker コンテナ内でのビルドという解決策が提示されたが、Azure Pipelines の方はどうなってるんだろう) というあたりは気になっている。 (まだ調べられていない。)