末尾に追記あり(2016/06/03)
MavenでWebアプリを作成しているときに、インテグレーションテスト(アプリを起動した状態で実際にリクエストしてみるテスト)をしたいときに便利な方法を知ったので自分で試してみた結果をまとめておきます。
内容については以下にあったものをまとめたものです。
- Configuring the Jetty Maven Plugin http://www.eclipse.org/jetty/documentation/current/jetty-maven-plugin.html#jetty-start-goal
- Maven Failsafe Plugin – Usage http://maven.apache.org/surefire/maven-failsafe-plugin/usage.html
- JUnitのCategoryさんとMavenのintegration-testでの実行 - 日々常々 http://irof.hateblo.jp/entry/20130411/p1
仕組み概要
簡単に仕組みを箇条書きでまとめます。
- Mavenのビルドの流れには
test
とは別にintegration-test
というフェイーズがある- 参照: Introduction to the Build Lifecycle http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
-
integration-test
はアプリの生成物をを作成したあとに、生成物を実際に外部から利用する形でのテストをするためのフェーズ -
integration-test
は事前作業pre-integration-test
と事後作業post-integration-test
をするフェーズがある -
integration-test
でテストが失敗しても終了しない。代わりにpost-integration-test
のあとのverify
フェーズにて失敗判定がおこなわれる- これは、事後作業の
post-integration-test
をテストの成否に関係なく実行するための仕組み
- これは、事後作業の
-
test
フェーズでのテスト実行はmaven-surefire-plugin
で行うのに対して、integration-test
ではmaven-failsafe-plugin
Maven Failsafe Pluginにて行う- これらのプラグインは1セットで開発されている様子
- 基本的にテストの実行処理は同じで、実行されるフェーズやデフォルトの設定が違うだけの様子
-
jetty-maven-plugin
では、このintegration-test
に対応するための機能を持っている- jettyサーバーの起動のみを行う処理(
jetty:start
やjetty:deploy-war
)と、サーバーの停止のみを行う処理(jetty-stop
)がある -
jetty:start
処理をpre-integration-test
フェーズに登録し、jetty:stop
処理をpost-integration-test
フェーズに登録することで、2フェーズ間のintegration-test
フェーズでjettyサーバーが起動している状態にできる
- jettyサーバーの起動のみを行う処理(
サンプル
実際に実用的さを意識したサンプルを以下に作成しました。
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>sample-app</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>Sample Webapp</name>
<!-- 全般設定 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<warfile.name>myapp</warfile.name>
<jetty.port>8081</jetty.port>
<report.dir>${project.build.directory}/reports</report.dir>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${warfile.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<reportsDirectory>${report.dir}</reportsDirectory>
<!-- integrationテストを対象外にする -->
<excludes>
<exclude>integration/**/*.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<summaryFile>${report.dir}/failsafe-summary.xml</summaryFile>
<reportsDirectory>${report.dir}</reportsDirectory>
<!-- レポートファイルの末尾に固定文字列をつける -->
<reportNameSuffix>IT</reportNameSuffix>
<!--
テスト対象クラスを指定。integrationパッケージ以下をテスト対象とする
デフォルトは"**/*IT.java"
-->
<includes>
<include>integration/**/*Test.java</include>
</includes>
<!-- システムプロパティでテスト実行用のパラメータを渡す -->
<systemPropertyVariables>
<!-- String url = System.getProperty("url"); で取得してテストで使う -->
<url>http://localhost:${jetty.port}/</url>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.3.7.v20160115</version>
<configuration>
<!-- 停止用の設定が必須 -->
<stopPort>8005</stopPort>
<stopKey>STOP</stopKey> <!-- 内容はなんでもいいっぽい -->
<!-- 実行ポートを指定 -->
<httpConnector>
<port>${jetty.port}</port>
</httpConnector>
</configuration>
<executions>
<!-- IT事前作業(pre-integration-test)に`jetty:deploy-war`(サーバーを立ち上げ処理)を登録 -->
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<!-- "start"でも良い -->
<goal>deploy-war</goal>
</goals>
<configuration>
<!-- 起動するwarファイル -->
<war>${project.basedir}/target/${warfile.name}.war</war>
<!--
ファイル変更検知によるリロード機能の検出間隔
テスト用起動なので機能OFFのための設定
-->
<scanIntervalSeconds>0</scanIntervalSeconds>
<!-- Deamonモードじゃないと処理が続けられない -->
<daemon>true</daemon>
</configuration>
</execution>
<!-- IT事後作業(post-integration-test)に`jetty:stop`(サーバー停止処理)を登録 -->
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!-- テスト結果レポートのHTMLを作るための設定 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<reportsDirectories>
<file>${report.dir}</file>
</reportsDirectories>
</configuration>
</plugin>
</plugins>
</build>
</project>
使い方は以下のようになります。
# UnitTestのみを実行する
mvn test
# UnitTest、package化、IntegrationTestを実行する
# mvn integration-test では無いことに注意。それだとpost-integration-testが実行されない
mvn verify
# テスト用jettyのポートを指定して起動する場合
mvn verify -Djetty.port=9090
# テスト結果レポートをhtml化する
mvn site -DgenerateReports=false # siteのcss,imageのみ生成する
mvn surefire-report:report-only
このサンプルでは記事最初で紹介したリンク先と同じことをしてもなんなので、以下のようなアレンジをしています。
- jettyの起動では
jetty:start
ではなく、jetty:deploy-war
を使っている-
jetty:start
はコンパイルされたtargetディレクリの中身をそのままつかってjettyを起動する -
jetty:deploy-war
はwar
オプションで指定したwarファイルのみを使ってjettyを起動する -
integration-test
フェーズの時点でpackage
フェーズが終わっているのでwarファイルが生成されているので、折角なのでそちらを使うようにしてみた - この方法を使えば、別のところで作成されたwarファイルを持ってきてテストで起動することもできる
-
- テスト結果レポートのファイル出力先を同じディレクトリにしている
-
test
フェーズ(surefire-plugin
)とintegration-test
フェーズ(failsafe-plugin
)のレポート出力先をreportsDirectory
で変更している - デフォルトだとこれらは別々のディレクトリに出力される
- しかし、結局はファイルの形式が同じjunitの結果なので1つにまとめた方が楽だと思う
- ただし、もし同じファイル名で出力してしまうと片方のが消えてしまうので、
reportNameSuffix
オプションでintegration-test
の結果を区別するようにしている
-
- テスト結果レポートのHTMLを生成している
-
maven-surefire-report-plugin
を使うとjunitのテスト結果からhtmlのファイルを生成できる -
mvn surefire-report:report
としてしまうと、test
フェーズまで実行されてしまうので注意 -
surefire-report-plugin
は、本来はsurefire-plugin
の結果に対応したものだが、failsafe-plugin
で出力した結果も同等に扱えるfailsafe-plutin
用には、本来はfailsafe-report-plugin
(mvn failsafe-report:report-only
)がある - テスト結果の出力先を変更しているので、
reportsDirectories
オプションで指定している
-
補足やうまくいかなかったこと
以降では、上記の補足と上手くいかなかったことを書き残しておきます
surefire-report:report-only
で生成されるHTMLで使うimageやCSSを生成する方法
上記の例でレポートhtml生成時に以下のコマンドを使ってcssとimageを生成しています。
mvn site -DgenerateReports=false # siteのcss,imageのみ生成する
report-onlyでは1つのHTMLファイルのみが生成されます。
このレポートHTMLは本来はsiteフェーズの一環として出力されるため、siteで生成されるcssやimageに依存しています。
しかし、siteフェーズを実行するとアプリ紹介ページなどの不要なものも作成されてしまいます。
色々試してみた結果、siteフェーズをgenerateReports=false
オプションをつけることでcssとimagesだけを生成できました。
私が調べた限りでは、これらリソースのみを生成するgoalは無さそうでした。
mvn site
にてテスト結果HTMLを生成するとHTMLがバグっている
mvn site
の中でsurefire-repot
によるHTMLを生成すると、なぜかHTML内で使われているJavascriptのメソッド定義がHTML中に出力されず、Javascriptで表示制御されている部分が動作しません。
そのため、mvn surefire-report:repot-only
で生成するようにしています。
mvn surefire-report:repot-only
のためのオプションはbuild
タグ内に記述する
surefire-reportは本来はsiteフェーズにて行うもののようです。
そのため、surefire-report-pluginの設定方法を調べると以下のようにreporting
タグ内に記載する方法がよく紹介されています。
しかし、ここでの設定はmvn site
を実行した時には適用されますが、mvn surefire-report:report-only
と直接実行した場合には適用されません。
もちろん逆もしかりで、build
タグ内にのみに記載した場合は、mvn site
実行時には適用されません。
<project>
・・・
<reporting>
<plugins>
<plugin>
<!-- テスト結果レポートのHTMLを作るための設定 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<reportsDirectories>
<file>${report.dir}</file>
</reportsDirectories>
</configuration>
</plugin>
</plugins>
</reporting>
・・・
</project>
インテグレーションテストのみを実行する方法がない
上記のサンプルのpom.xmlでは"テストを実行する"ということは、mvn verify
を実行するということになります。
mvnコマンドではサブコマンドとして指定したフェーズ名は、そのフェーズ"まで"を実行するという意味になります。
逆に、どのフェーズ"から"開始するかということが指定できません。
そのため、verify
を実行すると必ずtest
フェーズも実行されます。
これを避けて、test
フェーズを実行せずにintegration-test
のみを行う方法を調べたのですが、どうもオプションなどではできなさそうです。
mavenでは処理の開始フェーズを指定できないかわりに、各フェーズで実行されるここの処理(goal)に対してスキップ用のフラグが指定されていることが多いです。
test
フェーズで実際にテストを行うsurfire:test
処理にもスキップ用のフラグ(mvn実行時に"-D"で指定する)が3つもあります。(maven.test.skip
、maven.test.skip.exec
、skipTests
)
しかし、これらのオプションはそのままintegration-test
フェーズのfailsafe:integration-test
に対しても効いてしまうため、両方のテストがスキップされてしまいます。
逆に、integration-testだけをスキップするフラグならあったのですが... (skipITs)
ということで、integration-testのみの実行を行う方法はなさそうでした。
CIツールで実行するコマンドに困る
個人的にはCIツールでテストを実行するときには、test
もintegration-test
も必ず実行して欲しいと思っています。
ところが、mvn verify
ではtest
フェーズでテストが失敗するとその時点で処理が終了し、integration-test
が実行されません。
前章で記載した通り、integration-test
を単独で実行することができないので、2つのテストフェーズを別々に実行することもできません。
別の方向として、test
フェーズで失敗があってもそのまま処理を継続するためのフラグを使うことを考えました。
"-Dmaven.test.failure.ignore=true"オプションをつけて実行することでtest
、verify
の両フェーズでのテスト失敗を無視することができます。
しかし、この方法ではテストが失敗してもmvn
コマンドが正常終了になってしまうため、CIツールがテストに失敗があったことを検知できません。
テスト結果レポートのファイルを頑張ってパースすれば判定できなくは無いですが、それはちょっと大変そうです。
テストをスキップしてもサーバーは起動される
mvn
実行時にテスト実行をスキップしたいことはあると思います。
そのときは、実行時引数に-Dmaven.test.skip=true
などを指定することでテストの実行をスキップできます。
しかし、このオプションはあくまでsurefire:test
処理やfailsafe:integration-test
処理に対してのみの指定であるため、サーバーを起動・停止するjetty:start
やjetty:stop
には効果がありません。
そのため、テストは行わなくても無駄にサーバーが起動されてしまいます。
test
フェーズ実施時にサーバーを起動させる
jettyプラグインの設定において、jetty:start
とjetty:stop
の割当先フェーズを変更することで、test
フェーズ中にサーバー起動することもできます。
jetty:start
をtest
フェーズのひとつ前のprocess-test-classes
に、jetty:stop
をtest
フェーズ(登録順に実行されるようなのでtest
フェーズの最後に動きます)に割り当てます。
test
フェーズではまだpackage
フェーズを実行されていないので、warファイルを使うことができません。コンパイルは終わっているのでjetty:start
であれば動くようです。
しかし、この方式ではテスト失敗時にjetty:stop
が動きませんので、まま強引な方法かなと思います。
まとめ
個人的な印象ですが、mavenはいまいち良い感じの日本語での説明がありませんし、英語の公式文書にしても分かりにくいと思います。
様々な機能があるもののpluginとして独立しているためかplugin別に説明が分断されていて、pom.xmlの作成方法についての全体的で実践的な解説がいまいち少ないと感じています。
そのため、今回のintegration-test
のような標準でついている機能も、知名度がかなり低いのではないかと思います。
今回の記事でintegration-test
を紹介したものの、「mavenでのテスト実行といえばmvn test
を実行する」というのが常識になっているので、今回の方法によって「テスト実行はmvn vefify
になる」というのをふと忘れてしまいそうな感じもしているので、追加どころはちょっと悩んでもいます。
追記(2016/06/03)
integration-test
を使わず通常のtest
実行時にjettyサーバーを起動する方法を思いついたので追記しておきます。
テストの実行を以下のようなコマンドで行うと、サーバーを起動しつつテストができます。
mvn compile jetty:start test
jetty:start
コマンドは、jetty:run
と違ってjettyをdeamonとして起動します。そのため、jettyが起動されたあとに続けて後続のtest
タスクが処理されます。
本来は、jetty:start
と対になるjetty:stop
にて停止させるのですが、mvnコマンドそのものが終了すればjettyも停止されるので問題ありません。
ちなみに、jetty:run
は事前にコンパイルまで行われますが、jetty:start
はそうではないため明示的にcompile
タスクを実行する必要があります。