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 のマルチモジュールプロジェクトで JaCoCo の結果を集計する
Java / Kotlin のコードカバレッジツールとして JaCoCo を使いたい。 Gradle のマルチモジュールプロジェクトでの JaCoCo の導入について記す。
(この図は JaCoCo によるコードカバレッジの集計結果の履歴を Codecov で表示した例。)
JaCoCo について
JaCoCo は、JVM 言語のコードカバレッジツールである。 Java Agent によるオンザフライ方式のバイトコード instrumentation によるカバレッジ計測が可能である。
以前、Kotlin のコードカバレッジツールについて書いたので、こちらも参考にどうぞ。
Gradle プロジェクトで JaCoCo を使ったカバレッジ計測を行う
Gradle の JaCoCo プラグインを使うことで、Gradle のプロジェクトで JaCoCo を使ったコードカバレッジ計測を手軽に行うことができる。 ちなみに Gradle 4.9 の段階ではこのプラグインは incubating である。
Junit 5 のテスト実行時にカバレッジを計測する
各モジュールのテスト実行時にコードカバレッジを計測するだけなら非常に簡単で、単に JaCoCo プラグインを適用するだけで良い。
While all tasks of type
The JaCoCo Plugin - Gradle User ManualTest
are automatically enhanced to provide coverage information when thejava
plugin has been applied, any task that implementsJavaForkOptions
can be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.
バージョンを表す変数の定義や repositories
の定義などは省略しているが、おおよそ以下のようなビルドスクリプトで JUnit 5 でのテスト実行時に JaCoCo によるコードカバレッジの計測がなされる。
apply plugin: 'kotlin' apply plugin: 'jacoco' test { useJUnitPlatform() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompileOnly("org.junit.jupiter:junit-jupiter-api:$junit_version") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junit_version") }
デフォルトでは、カバレッジの計測結果は build/jacoco/test.exec というファイルに出力されるようである。
複数モジュールのコードカバレッジの集計
難しいのはここからで、複数モジュールでのコードカバレッジをどう集計するかで私は結構悩んだ。 JaCoCo の結果集計のためのタスクとしては JacocoMerge
というのがあるのだが、これをどう定義するのかが難しい。 (どのサブモジュールのカバレッジを集計するのかを明示的に指定するのであればそれほど大変ではないが、自動で全サブモジュールのカバレッジを集計するためのタスク定義をするのが難しい。)
結論としては、ルートプロジェクトに以下のようなタスク定義を行うことで対応した。 (ルートプロジェクトにも jacoco
プラグインを適用している。)
task jacocoMerge(type: JacocoMerge) { // ルートプロジェクトの評価時にはまだサブプロジェクトは存在しないので、 // 各プロジェクトの評価完了時に中の処理が走るように。 gradle.afterProject { p, state -> // ルートプロジェクトと `jacoco` プラグインが適用されていないプロジェクトは除く。 if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) { executionData p.tasks.test.jacoco.destinationFile dependsOn(p.tasks.test) } } }
大雑把に説明すると、jacocoMerge
タスクの executionData
に各サブモジュールのテスト実行時のカバレッジ計測結果を渡していき、かつ、jacocoMerge
タスクの依存に各サブモジュールの test
タスクを追加する、ということをしている。
gradle.afterProject
を使っている理由や if
文を使っている理由はコメントに書いてあるとおりである。
Gradle のプロジェクトの評価順については下記ページを参照されたい。
集計結果のコードカバレッジのレポーティング
さらにレポーティングのタスクは、ルートプロジェクトに下記のように定義することで行える。 java
プラグインが適用されている全サブモジュールのソースファイルをコードカバレッジのレポート対象に含める場合の例である。
task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) { executionData jacocoMerge.destinationFile sourceDirectories = files() classDirectories = files() gradle.afterProject { p, state -> if (p.rootProject != p && p.plugins.hasPlugin('java')) { sourceDirectories = sourceDirectories + files([p.sourceSets.main.allJava.srcDirs]) classDirectories = classDirectories + p.sourceSets.main.output } } reports { xml.enabled = true xml.destination file("${buildDir}/reports/jacoco/report.xml") html.destination file("${buildDir}/reports/jacoco/html") } }
本当は JacocoReport#sourceSets
を使いたかったが、内部で gradle.afterProject
を使われていて期待する挙動にならなかったので諦めた。
ちなみに上の xml.destination
の値は Codecov が期待するレポートファイルの位置である。
参考
- Gradleのマルチプロジェクト構成でJUnitやJacocoのレポートを集計する : プロジェクトの評価順を気にしなくてよければ
gradle.afterProject
を使わずにこういう感じで書ける。 - Aggregated Jacoco reports in a multi-project Gradle build · GitHub : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。
Java (JVM 言語) におけるコードカバレッジの計測方法
Java と Kotlin で書かれたアプリケーションのコードテストのカバレッジを取りたいなーと思って、コードカバレッジ計測ツールについて調べてみてる。
JVM 言語におけるコードカバレッジ計測の方法
JaCoCo のドキュメントにわかりやすくまとまっている。
Coverage information has to be collected at runtime. For this purpose JaCoCo creates instrumented versions of the original class definitions. The instrumentation process happens on-the-fly during class loading using so called Java agents.
There are several different approaches to collect coverage information. For each approach different implementation techniques are known. The following diagram gives an overview with the techniques used by JaCoCo highlighted:
JaCoCo - Implementation Design
カバレッジ情報は実行時に収集されるものなので、情報収集のための仕組みが必要。
大きく分けると Runtime Profiling と Instrumentation の 2 つの仕組みがある。 前者は JVM の仕組み (JVMTI や JVMPI) を使うもの。 後者は実行対象のプログラムの方に収集用の仕組みを搭載する (これを instrumentation というらしい?) もの。
Runtime Profiling
JVM TI は Java Virtual Machine Tool Interface の略。 Java SE 5 で導入された。
JVM TI は JVM 上で動くアプリケーションの状態を検査したり、実行を制御したりするためのプログラミングインターフェイスらしい。
JVMPI は JVM TI よりも古くからある同じようなインターフェイスらしい。 Java SE 5 で (JVM TI ができたことで) 非推奨になり、Java SE 6 で廃止された模様。
これらを使ったコードカバレッジは、アプリケーション側に何も手を入れなくて良いことが利点だと思われる。 が、詳細はわからない。
Instrumentation
上でも説明したように、計測対象のアプリケーションコードに情報収集のための仕組みを搭載する方法が、この Instrumentation である。 Runtime Profiling と異なり、Android アプリの実行環境のような非 JVM 環境でも使用できるという利点がありそう *1。
上の図を見るとわかるように、様々な方法がある。
JaCoCo は、Java Agent を用いてオンザフライでバイトコードを変更する方式である。 *2
Java Agent について
Java Agent については java.lang.instrument
パッケージの Javadoc に書かれている。
-javaagent:
というコマンドラインオプションで指定して使用できるものである。 次のページも参考になる。
エージェントの背景にある基本概念は、「JVM がクラスをロードする場合、エージェントはそのクラスのバイトコードを修正できる」 という考え方です。
様々なコードカバレッジツール
JVM 言語用の様々なコードカバレッジツールについて、Clover のブログで比較紹介してくれている。 (最終更新が 2017 年春なので情報はちょっと古いかもしれない。)
有名どころとしては JaCoCo、OpenClover (Atlassian Clover がオープンソース化されたもの)、JCov といったところだと思う。
Instrumentation 方式に着目すると、JaCoCo と JCov はオフラインおよびオンザフライのバイトコード instrumentation に対応しており、Clover はソースファイル instrumentation に対応している。 なので JaCoCo や JCov は (JVM 言語なら何でも対応できるので) Kotlin にも対応するが、Clover は (対応言語に入っていない) Kotlin には対応しない。
Clover と JaCoCo を軽く使ってみたところ、設定の簡単さはどちらも同じ。 出力される HTML を見ると JaCoCo は単純な内容で、Clover の方はプロジェクトリスクの高いものを表示したり、視覚的だったりと、結果表示については高機能さを感じた。
今回は Kotlin でのカバレッジも取りたいので、(対応していない) Clover は選外で、JaCoCo か JCov のどっちかを使うことになりそう。
Gradle のマルチモジュールプロジェクトで maven-publish プラグインを使う場合の依存関係
Gradle でビルド成果物 (build artifact) を Maven リポジトリに公開するためのプラグインとして、maven-publish プラグインがある。
maven-publish プラグインとマルチプロジェクトの依存関係
簡単な例
Java ライブラリのプロジェクトがあったとして、JAR ファイルを Maven リポジトリに公開するための build.gradle の記述は以下のようになる。
// Java ライブラリのビルド用の設定などは省略 apply plugin: 'maven-publish' publishing { publications { mavenJava(MavenPublication) { from components.java } } }
これで publishToMavenLocal
タスクなどが使えるようになるので、例えばローカルホスト上の Maven リポジトリに公開するには ./gradlew publishToMavenLocal
コマンドを実行すればよい。
依存関係
上の例では、ソフトウェアコンポーネントとして java
コンポーネントを指定している。 このとき、依存関係としては runtime
コンフィギュレーションのものが使われる。
Maven Publishing (new) - Gradle User Manual
Name Provided By Artifacts Dependencies java The Java Plugin Generated jar file Dependencies from 'runtime' configuration web The War Plugin Generated war file No dependencies
マルチモジュールプロジェクトでの依存関係
例えば :foo
プロジェクトと :bar
プロジェクトからなるマルチモジュールプロジェクトで、:bar
プロジェクトから :foo
プロジェクトに依存している場合に、出力される pom.xml に記載される依存関係はどうなるのか?? 具体的には bar/build.gradle に以下のように書かれている状況である。
dependencies {
compile project(':foo')
}
これはドキュメントには書かれていないが、maven-publish プラグインのソースコードを確認したところ、依存先プロジェクトの設定に応じて自動的に依存先ライブラリの指定がなされるようになっていた。
例えば、依存先プロジェクトで maven-publish プラグインが使われていない場合は、プロジェクトのグループやプロジェクト名、バージョンが使われるようである。 依存先プロジェクトで maven-publish プラグインが使われており、特定の条件を満たす publication が存在する場合は、その publication で指定されているグループ・artifactId・バージョンが使われる。
publishing { publications { maven(MavenPublication) { groupId 'org.example' artifactId 'foo-sample' version '1.1' from components.java } } }
foo/build.gradle に上のように書かれていたら、./gradlew :bar:publishToMavenLocal
タスクで公開される成果物の pom ファイルには以下の内容が含まれることになる。
<dependency> <groupId>org.example</groupId> <artifactId>foo-sample</artifactId> <version>1.1</version> <scope>compile</scope> </dependency>
気を付ける必要があること
依存先プロジェクトの中で maven-publish
プラグインが使用されて、publication が定義されている必要がある。
下記のように、親プロジェクトで子プロジェクトの publication をまとめて定義することもできるのだが、そうすると依存関係の解決時に 「依存先プロジェクトでは publication がない」 という扱いになってしまって、デフォルト値が使用される。
publishing { publications { mavenFoo(MavenPublication) { groupId 'org.example' artifactId 'foo-sample' version '1.1' from findProject(':foo').components.java } mavenBar(MavenPublication) { groupId 'org.example' artifactId 'bar-sample' version '1.1' from findProject(':bar').components.java } } }
私はこれでハマってしまった。 (Exposed の build.gradle を見て、親の build.gradle で子プロジェクトの publication を宣言していたので、それを参考にした、という。) 注意されたし。
Java Persistence API (JPA) の JPQL で集計処理を書く (Spring Data JPA)
Spring Data JPA を使っていて、集計処理をどのように書くのが良いかわからずに調べた記録。 (例は Spring Data JPA 固有の話ではあるが、JPQL の知識は Spring Data JPA 以外での JPA でも使える。)
Spring Data JPA での集計の例
リポジトリにメソッドを定義して @Query
アノテーション使うことで、JPQL (Java Persistence query language) によるクエリを記述できる。 これを用いて、例えば FooRepository
に Foo
に関する集計処理を記述できる。
@Query(value = """ SELECT NEW com.example.AggregationResult( FUNCTION('year', foo.date), COUNT(foo) ) FROM Foo foo LEFT JOIN foo.bar bar LEFT JOIN foo.baz baz WHERE foo.targetId = ?1 AND (bar.content IS NOT NULL OR baz.content IS NOT NULL) GROUP BY FUNCTION('year', foo.date) """) fun aggregateCountPerYear(targetId: Long): List<AggregationResult>
上のコードは Kotlin で記述されたメソッド定義の例である。 (FUNCTION('year', foo.date)
が 2 箇所にあるのが微妙だし仕様上正しく動くものなのかわからないので何とかしたかったけど、どう書くのがいいかわからなかったのでこの形になっている *1。 誰か詳しい人が居たら教えてください><)
上記の例に含まれる JPQL の知識を下記に述べる。
集計に使える JPQL の知識
参照した JPA 仕様
学び
SELECT
句内のコンストラクタ式 (JPA 2.2 仕様の 4.8.2 節)
JPQL の SELECT
句の項目として Java クラスのコンストラクタを記述できる。 集計結果をオブジェクトで返したいときに便利。 ちなみに対象のクラス名は完全修飾名で指定する必要がある。
データベース関数の実行 (JPA 2.2 仕様の 4.6.17.3 節)
データベースの関数を FUNCTION(function_name, arg1, arg2, ...)
の形で記述できる。 JPQL で定義されていない集計関数を用いたい場合や、ユーザー定義の関数を利用したい場合などに便利。
LEFT JOIN
(JPA 2.2 仕様の 4.4.5.2 節)
LEFT JOIN
と LEFT OUTER JOIN
はシノニムである。 JPA においては、エンティティ定義でエンティティ間の関連を定義して、JPQL では 単純に下記のようにプロパティ名の記述だけで JOIN
を記述できる。
FROM Foo foo JOIN foo.bar bar
ただし、上記のコードでは foo.bar
が null
となるような Foo
は処理対象から外れてしまう。 foo.bar
が null
でも処理対象とするために LEFT JOIN
を使用する必要がある。 普通に SQL を書く場合には LEFT JOIN
を使うのは自然だと思うが、JPQL だと JOIN
の条件を書かなかったり、Foo
のエンティティを普通に引っ張ってくるときは bar
が null
でも取得できたりするので、LEFT JOIN
が必要なことをうっかり忘れがちになりがちな気がする。 注意が必要。
参考
- JavaEE7をはじめよう(4) - JPAクエリ(その1) JPQL - エンタープライズギークス (Enterprise Geeks) : JPQL の全体的な話。 参考になる。
- わかりやすい JPA(7)関数と集計クエリ : JPQL での集計の話。 参考になる。
- JPQLの集計関数に価値はあるか? - taediumの日記 : 集計には JPQL ではなく SQL を使った方が良いのではないかという話。 確かに……。 JPQL を使うとエンティティ定義を扱えるので、JPA の世界の中で集計クエリを書けるという利点はあるような気がする。 (多少メンテナンス性は良くなる気がするので、JPQL で困らないなら JPQL で書くと良さそうな気はしている。)
*1:テストは書いてあるのでまあ大丈夫かなという……。