Edited at

MavenでアプリをJetty起動しながらintegrationテストを実行

More than 3 years have passed since last update.

末尾に追記あり(2016/06/03)


MavenでWebアプリを作成しているときに、インテグレーションテスト(アプリを起動した状態で実際にリクエストしてみるテスト)をしたいときに便利な方法を知ったので自分で試してみた結果をまとめておきます。

内容については以下にあったものをまとめたものです。


仕組み概要

簡単に仕組みを箇条書きでまとめます。


  • Mavenのビルドの流れにはtestとは別にintegration-testというフェイーズがある




  • 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:startjetty:deploy-war)と、サーバーの停止のみを行う処理(jetty-stop)がある


    • jetty:start処理をpre-integration-testフェーズに登録し、jetty:stop処理をpost-integration-testフェーズに登録することで、2フェーズ間のintegration-testフェーズでjettyサーバーが起動している状態にできる




サンプル

実際に実用的さを意識したサンプルを以下に作成しました。


pom.xml

<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-warwarオプションで指定した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実行時には適用されません。


pom.xml

<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.skipmaven.test.skip.execskipTests)

しかし、これらのオプションはそのままintegration-testフェーズのfailsafe:integration-testに対しても効いてしまうため、両方のテストがスキップされてしまいます。

逆に、integration-testだけをスキップするフラグならあったのですが... (skipITs)

ということで、integration-testのみの実行を行う方法はなさそうでした。


CIツールで実行するコマンドに困る

個人的にはCIツールでテストを実行するときには、testintegration-testも必ず実行して欲しいと思っています。

ところが、mvn verifyではtestフェーズでテストが失敗するとその時点で処理が終了し、integration-testが実行されません。

前章で記載した通り、integration-testを単独で実行することができないので、2つのテストフェーズを別々に実行することもできません。

別の方向として、testフェーズで失敗があってもそのまま処理を継続するためのフラグを使うことを考えました。

"-Dmaven.test.failure.ignore=true"オプションをつけて実行することでtestverifyの両フェーズでのテスト失敗を無視することができます。

しかし、この方法ではテストが失敗してもmvnコマンドが正常終了になってしまうため、CIツールがテストに失敗があったことを検知できません。

テスト結果レポートのファイルを頑張ってパースすれば判定できなくは無いですが、それはちょっと大変そうです。


テストをスキップしてもサーバーは起動される

mvn実行時にテスト実行をスキップしたいことはあると思います。

そのときは、実行時引数に-Dmaven.test.skip=trueなどを指定することでテストの実行をスキップできます。

しかし、このオプションはあくまでsurefire:test処理やfailsafe:integration-test処理に対してのみの指定であるため、サーバーを起動・停止するjetty:startjetty:stopには効果がありません。

そのため、テストは行わなくても無駄にサーバーが起動されてしまいます。


testフェーズ実施時にサーバーを起動させる

jettyプラグインの設定において、jetty:startjetty:stopの割当先フェーズを変更することで、testフェーズ中にサーバー起動することもできます。

jetty:starttestフェーズのひとつ前のprocess-test-classesに、jetty:stoptestフェーズ(登録順に実行されるようなので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タスクを実行する必要があります。