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

末尾に追記あり(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](http://maven.apache.org/surefire/maven-failsafe-plugin/index.html)にて行う
  * これらのプラグインは１セットで開発されている様子
  * 基本的にテストの実行処理は同じで、実行されるフェーズやデフォルトの設定が違うだけの様子
* [`jetty-maven-plugin`](http://www.eclipse.org/jetty/documentation/current/jetty-maven-plugin.html)では、この`integration-test`に対応するための機能を持っている
  * jettyサーバーの起動のみを行う処理(`jetty:start`や`jetty:deploy-war`)と、サーバーの停止のみを行う処理(`jetty-stop`)がある
  * `jetty:start`処理を`pre-integration-test`フェーズに登録し、`jetty:stop`処理を`post-integration-test`フェーズに登録することで、２フェーズ間の`integration-test`フェーズでjettyサーバーが起動している状態にできる



# サンプル

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


```xml: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>
```

使い方は以下のようになります。

```bash
# 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の結果なので１つにまとめた方が楽だと思う
  * ただし、もし同じファイル名で出力してしまうと片方のが消えてしまうので、`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を生成しています。

```bash
mvn site -DgenerateReports=false # siteのcss,imageのみ生成する
```

report-onlyでは１つの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`実行時には適用されません。

```xml: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"で指定する)が３つもあります。(`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`を単独で実行することができないので、２つのテストフェーズを別々に実行することもできません。

別の方向として、`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サーバーを起動する方法を思いついたので追記しておきます。

テストの実行を以下のようなコマンドで行うと、サーバーを起動しつつテストができます。

```bash
mvn compile jetty:start test
```

`jetty:start`コマンドは、`jetty:run`と違ってjettyをdeamonとして起動します。そのため、jettyが起動されたあとに続けて後続の`test`タスクが処理されます。
本来は、`jetty:start`と対になる`jetty:stop`にて停止させるのですが、mvnコマンドそのものが終了すればjettyも停止されるので問題ありません。

ちなみに、`jetty:run`は事前にコンパイルまで行われますが、`jetty:start`はそうではないため明示的に`compile`タスクを実行する必要があります。