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 の方はどうなってるんだろう) というあたりは気になっている。 (まだ調べられていない。)
JerseyTest で Servlet のリソースを扱う JAX-RS 部品のユニットテストを行う方法
要約
前提知識 : Servlet と JAX-RS
Jakarta EE (旧 Java EE) における HTTP サーバーアプリケーションのための API として有名なもの *1 は Servlet と JAX-RS がある。 これらは依存関係にはなく、Servlet 単体でも JAX-RS 単体でも使用できる。
一方で、JAX-RS を Servlet コンテナ内で実行し、JAX-RS コンポーネントに Servlet のコンポーネントをインジェクトするというような使い方もできる。
- Servlet 4.0 : The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 369
- JAX-RS 2.1 : The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 370
In a product that also supports the Servlet specification, implementations MUST support JAX-RS applications that are packaged as a Web application. See Section 2.3.2 for more information Web application packaging.
JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environments - 11.2.1 Servlets)
The
JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environment - 11.1 Servlet Container)@Context
annotation can be used to indicate a dependency on a Servlet-defined resource. A Servlet-based implementation MUST support injection of the following Servlet-defined types:ServletConfig
,ServletContext
,HttpServletRequest
andHttpServletResponse
.
Servlet のリソースを扱う JAX-RS コンポーネントをユニットテストする方法 (JerseyTest を使用)
JAX-RS コンポーネントをユニットテストする方法は様々あるだろうが、個人的には JAX-RS の参照実装である Jersey のテストフレームワークを使う方法に親しみがある。
- Jersey のテストフレームワーク : Chapter 26. Jersey Test Framework
Jersey のテストフレームワークは JerseyTest
というクラスを提供しており、これを継承してテストクラスを作ると簡単に JAX-RS コンポーネントのテストを記述できる。
public class SimpleTest extends JerseyTest { // SimpleResource という JAX-RS リソースをテストしたい場合は、 // それを ResourceConfig に渡してテスト用の Application を作ればよい。 @Override protected Application configure() { return new ResourceConfig(SimpleResource.class); } // テストメソッド @Test public void test() { final String responseContent = target("simple").request().get(String.class); assertEquals("Hello World!", responseContent); } }
問題 : 単に JerseyTest を継承してテストクラスを定義するだけだと Servlet リソースが扱われない
上のようなテストコードを書いた場合、基本的には問題なくテストが動く。 しかし、SimpleResource
が Servlet のリソースを扱っている場合 (@Context
で HttpServletRequest
をインジェクトしている場合など) には、期待通りに動かない。
なぜなら、(おそらく) デフォルトで選択されるテストコンテナは Servlet をサポートしておらず、Servlet のリソースのインジェクトが行われないからである。
解決策 : Servlet をサポートしているテストコンテナを利用する
テストコンテナの決まり方については、ドキュメントに下記のように書かれている。
A test container is selected based on various inputs.
Chapter 26. Jersey Test FrameworkJerseyTest#getTestContainerFactory()
is always executed, so if you override it and provide your own version ofTestContainerFactory
, nothing else will be considered. Setting a system variableTestProperties#CONTAINER_FACTORY
has similar effect. This way you may defer the decision on which containers you want to run your tests from the compile time to the test execution time. Default implementation ofTestContainerFactory
looks for container factories on classpath. If more than one instance is found and there is a Grizzly test container factory among them, it will be used; if not, a warning will be logged and the first found factory will be instantiated.
一番簡単な方法は JerseyTest#getTestContainerFactory()
をオーバーライドすることだろう。
Servlet をサポートしているテストコンテナもドキュメントに書かれている。
Second factory is
Chapter 26. Jersey Test FrameworkGrizzlyWebTestContainerFactory
that is Servlet-based and supports Servlet deployment context for tested applications. This factory can be useful when testing more complex Servlet-based application deployments.
GrizzlyWebTestContainerFactory
を使えばよい。 ちなみに GrizzlyWebTestContainerFactory
は DeploymentContext
として ServletDeploymentContext
を求めるので、その点は注意が必要である。
ということで、下記のようなテストクラスを書けば Servlet のリソースを扱う JAX-RS のユニットテストを記述できる。
import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.ServletDeploymentContext; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.glassfish.jersey.test.spi.TestContainerException; import org.glassfish.jersey.test.spi.TestContainerFactory; public class CookieSessionResponseFilterTest extends JerseyTest { private TestContainerFactory testContainerFactory; /** * テスト用のコンテナを生成するメソッドをオーバーライド。 * * ここで {@link GrizzlyWebTestContainerFactory} を指定することで、サーブレットベースのアプリケーションのテストが可能となる。 */ @Override protected TestContainerFactory getTestContainerFactory() throws TestContainerException { // こういう感じで同じインスタンスを返す必要があるのかはよくわからないが // JerseyTest の実装がそういう感じになっているのでそれに合わせている。 if (testContainerFactory == null) { testContainerFactory = new GrizzlyWebTestContainerFactory(); } return testContainerFactory; } /** * テスト用のコンテナにデプロイする内容。 * * {@link GrizzlyWebTestContainerFactory} を使うためには {@link ServletDeploymentContext} を返す必要がある。 */ @Override protected ServletDeploymentContext configureDeployment() { ServletContainer jaxRsServlet = ; return ServletDeploymentContext // JAX-RS コンポーネントは `ServletContainer` に包んでやる。 .forServlet(new ServletContainer(configure())) // Servlet フィルタの追加も可能。 .addFilter(TestServletFilter.class, TestServletFilter.class.getSimpleName()) .build(); } // SimpleResource という JAX-RS リソースをテストしたい場合は、 // それを ResourceConfig に渡してテスト用の Application を作ればよい。 // {@link #configureDeployment()} をオーバーライドしたらこのメソッドを // オーバーライドしなくても良いはず (configureDeployment 内に書けばよいはず) だが、 // 一応オーバーライドして {@link #configureDeployment()} から呼ぶようにした。 @Override protected ResourceConfig configure() { return new ResourceConfig(SimpleResource.class); } // テストメソッド @Test public void test() { final String responseContent = target("simple").request().get(String.class); assertEquals("Hello World!", responseContent); } }
自分でテストコンテナのファクトリを書いても良い
上ではもともと Jersey のテストフレームワークに用意されている GrizzlyWebTestContainerFactory
を利用したが、独自に TestContainerFactory
を実装しても良い。 サーブレットを複数含むテストを書きたい場合などにはそうする必要がありそうである。 (そもそもそういうテストを書く必要がある設計にすべきではないという気もするが。)
『JerseyTestでHttpServletRequestを使いたい - Chonaso's Commentary』 が参考になる。 (実際に自分で書いたらわりと手を入れる必要はあったが。)
おわり
まとめると、下記のとおり。
とはいえ、JAX-RS で Servlet のリソースを扱う必要がある場面は非常に限定的であるはずで、JAX-RS の API だけで完結できる場面ではそうすべきである。 その方がテストも書きやすいし移植性も高い。 複数の API 仕様の知識 (JAX-RS の知識も Servelt の知識も必要) を求めることもなくなるので、開発者に対しても優しいはずである。
仕事で触っているコードには JAX-RS の中で Servlet のリソースを扱っているものがわりとあるのだが、少しずつ JAX-RS の API に置き換えていきたい。
*1:これ以外にあるのかどうかは知らない。
Gradle 4.9 の新しいタスク定義 API と Gradle Kotlin DSL 1.0 RC での対応
Gradle 4.9 の新しいタスク定義 API
Gradle 4.9 で、新しいタスク定義 API が導入されました。 まだ incubating です。
パフォーマンス向上のため、タスクの設定を遅延実行する、というのがこの新しい API の導入の目的のようです。 古い API では Task
インスタンスをすぐに生成していたのを、新 API ではまず Provider<Task>
を生成して、必要になった段階で Task
インスタンスを生成する、という形になります。
Gradle Kotlin DSL 1.0 RC での対応
ちょうど昨日 Gradle 4.10 がリリースされましたね! めでたい!
Gradle 4.10 is out! https://t.co/wmo5e8eb8y
— Gradle (@gradle) 2018年8月27日
⚡️ Incremental @java compilation by default
📤 Periodic Gradle cache cleanup
🚀 @Kotlin DSL 1.0 RC
📦 Plugins DSL supports SNAPSHOT versions pic.twitter.com/5oNXRYJgra
Gradle 4.10 には Gradle Kotlin DSL 1.0-RC3 が載っており、Kotlin DSL の方でも新しいタスク定義 API がサポートされています。
Release 1.0-RC3 · gradle/kotlin-dsl · GitHub
- introducing the new existing and registering delegated properties designed specifically with configuration avoidance in mind which expose
NamedDomainObjectProvider<T>
instead of the container element type.
下記のように使用できます。
// 既存タスクの設定。 val test by tasks.existing(Test::class) { ... } // 新規タスクの宣言と設定。 val jacocoMerge by tasks.registering(JacocoMerge::class) { ... }
使ってみた
早速使ってみました。
Update Gradle (version 4.10) by nobuoka · Pull Request #13 · nobuoka/wd-image-processor · GitHub
小さいプロジェクトなので特にパフォーマンスの向上などは感じられませんが、移行自体は苦労なくできます。