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
小さいプロジェクトなので特にパフォーマンスの向上などは感じられませんが、移行自体は苦労なくできます。
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 : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。
Windows 10 で Git 環境を整える
自分用メモ。 昔は GitHub Desktop をインストールすれば Git Shell も使えるようになって PowerShell で快適 Git 生活が送れていたのだけど、最近 GitHub Desktop をインストールしてみたところ、どうやら Git Shell は同梱されないようになってしまったっぽい。
今ならこうすると良さそう、というメモ。
Git 環境を整える
GitHub Desktop インストール
人によっては全然いらないと思うけど、あったらあったで便利なので。 (追記 : 2019 年 5 月現在、個人的にこれはなくていいかなーと思いつつある。 特に使わないので。)
GitHub Desktop | Simple collaboration from your desktop
起動したら GitHub にログイン。 それで Git の設定でユーザー名やメールアドレスが自動的に設定されるので便利。
Git for Windows をインストール
GitHub Desktop にも git
コマンドは同梱されているけど、GitHub Desktop とは独立してバージョンアップなどをできるように Git for Windows をインストールしておく。
Git - Downloading Package (Git 本家サイトの Git for Windows のダウンロードページ)
Git for Windows (こっちは Git for Windows のサイト)
インストール途中の選択肢で、push 時や pull 時の改行コードの変換をどうするか聞いてくれる。 が、as-is を選んでも改行コードが変換されないようにならなかったことがある。 (後でやったときはちゃんと設定された。) また、GitHub Desktop をインストールしない場合にはユーザー名やメールアドレスを設定する必要がある。
# 設定が必要なら user.name=.... user.email=.... # core.autocrlf の設定がなされていないならば git config --global core.autocrlf false
posh-git をインストール
PowerShell で Git が便利になる。 タブでサブコマンドやブランチ名の補完をしてくれたり、プロンプトに現在のブランチを表示してくれたりする。 (昔の GitHub Desktop に同梱されていた Git Shell で PowerShell を使う場合には自動で有効になってた。)
posh-git by dahlbyk
Git - PowershellでGitを使う
書かれてる通りにインストール。
必要な設定
GitHub に SSH 鍵登録
Generating a new SSH key and adding it to the ssh-agent - GitHub Help
Adding a new SSH key to your GitHub account - GitHub Help
文字化け (?) 対応
初期状態だと git log
などで日本語の表示がおかしくなる。 文字化けというか、バイト値が 16 進数表記で連なる感じ。
git config --global core.pager "LESSCHARSET=utf-8 less"
という感じで、config の core.pager
を 「LESSCHARSET=utf-8 less」 に設定すれば良さそう。
~\Documents\test [master]> git log --graph --decorate * commit a5e5b4d21432f3fcba4ffbf1dd0d4bc598c4b051 (HEAD -> master) Author: Nobuoka Yu <nobuoka@vividcode.info> Date: Wed Aug 8 23:33:26 2018 +0900 <E3><81><93><E3><82><93><E3><81><AB><E3><81><A1><E3><81><AF> ~\Documents\test [master]> git config --global core.pager "LESSCHARSET=utf-8 less" ~\Documents\test [master]> git log --graph --decorate * commit a5e5b4d21432f3fcba4ffbf1dd0d4bc598c4b051 (HEAD -> master) Author: Nobuoka Yu <nobuoka@vividcode.info> Date: Wed Aug 8 23:33:26 2018 +0900 こんにちは
読んだ : 入門 Kubernetes
Kubernetes に興味を持ちつつ何も手を付けていなかったのだけど、会社に置かれてたのでまずは本を読んだ。
- 作者: Kelsey Hightower,Brendan Burns,Joe Beda,松浦隼人
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/03/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
Kubernetes を何故使うのか、というところから始まり、Kubernetes における各種オブジェクトの大まかな概念の説明から使い方まで一通り解説されている。
読書メモ
Kubernetes を何故使うのか、という導入では以下のような内容が書かれている。
- ベロシティ : 宣言的設定でイミュータブルなコンテナを扱うことで自己回復しやすくスケールもしやすくなっている。
- サービスとチームのスケール : マイクロサービス化による開発チームのスケールや、アプリケーションやクラスタのスケールをしやすくする、など。
- インフラの抽象化・アプリケーション実行のためのプラットフォーム : 『プロダクションレディマイクロサービス』 で言われていた 4 層モデルの 「アプリケーションプラットフォーム」 に該当する。 アプリケーションプラットフォーム側がアプリケーション開発側に対して API を提供することで、アプリケーションプラットフォーム側とアプリケーション側の責務を切り離す。
- インフラの効率性。 アプリケーションのコンテナを同じ物理マシン (または仮想マシン) 上に同居させることができる。
便利。
プロダクションレディマイクロサービス ―運用に強い本番対応システムの実装と標準化
- 作者: Susan J. Fowler,佐藤直生,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/09/13
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
それから、「Pod」 や 「ReplicaSet」、「DaemonSet」、「Deployment」、「Job」、「Service」、「Namespace」 といった概念について、本を通して解説される。 下記に今のところの自分の理解を書いておく。
- Pod : コンテナの集まり。 Web サービスのアプリケーションの 1 インスタンスなどが Pod になる。
- ReplicaSet : 特定種類の Pod の集まり。 Web サービスを複数インスタンスで動かす際などには ReplicaSet で複数 Pod を動かす、という形になる。
- Deployment : ReplicaSet の履歴を保持したオブジェクト。 Web サービスに変更を加える際には、新バージョンに対応する ReplicaSet を Deployment に追加してアップデートを行う、という形。
- Service : Kubernetes クラスタ上のアプリケーション同士の通信や、外部への公開のために使われる。
良かった
Kubernetes に入門するには良い本だった。 著者は Kubernetes の開発者なので、どういう思想でこうなっているのか、というのもところどころに書かれていて 「なるほどねー」 と言いながら読める。 本書をざーっと読んで概念を理解したら、あとは実際に Kubernetes を触って動かしていくと良さそう。 今なら Docker for Windows で Kubernetes クラスタを簡単に用意できたりするので、軽く試すだけなら簡単にできる。
Kubernetes やっていこう。