読んだ : JUnit 実践入門 〜 体系的に学ぶユニットテストの技法
JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)
- 作者: 渡辺修司
- 出版社/メーカー: 技術評論社
- 発売日: 2012/11/21
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 273回
- この商品を含むブログ (69件) を見る
既に JUnit 5 が出ているし今更感はある (本書は JUnit 4 を題材にしている) けど、テストの全体的な話も学べるかなって思って読んだ。 JUnit 4 の話もありつつテスト全般に関する知見も書かれているので、(類書も少ないので) 読む機会がある人はさらっと読んでみるといいかもしれない。
新しく学んだこと
テスト全般
- テストフィクスチャ (test fixtures) : テストの対象やテストの入力値や検証用の値、外部リソースなどの実行環境やテストの実行前に必要なオブジェクトの操作などのこと。
- 狭義にはテストデータだけを指して 「テストフィクスチャ」 と呼ぶこともあるらしい。
- 振舞駆動開発 (behavior driven development; BDD) : ソフトウェアの相互作用に着目してソフトウェアがどのように振る舞うかを定義することを起点とした開発技法
JUnit 4 について
Theories
ランナーでパラメータ化テストができる。- Cucumber / cucumber-junit で振舞駆動開発を実現できる。
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 )
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 : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。
Espresso 2.0 が Android support library の一部としてリリースされた
上記エントリにあるように、Espresso 2.0 がリリースされた。 Espresso は Android アプリの自動テストのためのライブラリである。 ほぼ上のエントリに書かれている内容であるが、Espresso 2.0 のリリースについて紹介する。
変更内容など
一番大きな変更は、Android support library の一部になったことだと思われる。 (上のエントリにもそう書かれている。) *1 そのおかげで、Android SDK で 「Android Support Repository」 をインストールしておけば (JAR をダウンロードしたりせずに) 簡単に使用できるようになった。
API 的には、パッケージ名の変更以外は大きな変更点はなさそうである。 とはいえ一部非互換な変更が加わっているので、そこら辺は気を付ける必要がある。 変更内容はリリースノートを見ると良い。
また、Instrumentation テストランナーとして GoogleInstrumentationTestRunner
にいくつかの機能を追加した AndroidJUnitRunner
が含まれている。 JUnit 4 サポートも含まれていて、これを使うことで JUnit 4 を使ったテストを書けるようになる。
ドキュメント
2015 年には Android Developers の方にドキュメントが移される予定のようだが、今のところはまだ android-test-kit でホストされている。 2016 年現在、ドキュメントは既に移されている。
- Android Developers 内のドキュメント : Testing Support Library | Android Developers
- Testing support library の GitHub Page : Android Testing Support Library
Testing Support Library の Javadoc は以下。
サンプルコード
サンプルプロジェクトが GitHub にホストされている。 JUnit 4 を使ったテストの例などもあり、参考になる。
また、私が公開している Espresso を使用したテストの例も Espresso 2.0 に対応させた。
「Espresso を使って PreferenceActivity の自動 UI テストを行う」 のエントリを書いたときに作ったサンプルプロジェクトである。
関連エントリ
JUnit 4 によるテストの説明や紹介をしているエントリがあります。
変更履歴
- 2016-10-02 : リンク先が Not Found になっているものがあったので、リンク先を変更しました。
*1:早く Volley も support library の一部にならないかな。