日々常々

ふつうのプログラマがあたりまえにしたいこと。

JUnitのCategoryさんとMavenのintegration-testでの実行

詳しいことは JUnit実践入門 の 10章 カテゴリ化テスト を読んでください。
ざっくり言えば Category アノテーション 付けてテストをカテゴライズできるよひゃっほーって話。*1

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

テストをカテゴライズしつつ、かつ単体での実行時に影響を与えないので、Maven などのビルドツールを使用してまとめてテストを実行する時に効果を発揮するものです。何気に使いどころは難しかったりするのですが、選択肢として道具箱に入れていることをお勧めします。

この手のことをする時、従来ではテストクラス名をネーミングルールで縛ったり、ソースディレクトリを分けたりなどしていました。これらは現在でも有効ですが、カテゴリはもっと細かい単位で分けたい時とか、複数のカテゴリを指定するタグのような使い方が出来ます。一長一短はありますので、天秤にかけた上で適した方を選んでください。

書き方

import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(HogeTests.class)
public class FugaTest {
    @Test ...

テストクラスに Category アノテーションを付けて、カテゴリに対応したマーク用のクラスを values に渡す。複数も可能。これだけでそのカテゴリに属したテストクラスとマークされる。IDEから実行するときは効果無しなんで、単独で実行するときは邪魔になりません。なので「全体で実行するときは無視したいけど、指定したときは動いて欲しい」なテスト*2に付けとくと有効な場面もあります。たぶん。この手のに @Ignore とか付けちゃうと、それだけで動かそうにも出来ませんしね。

Maven で Category を扱う

maven-surefire-plugin の groups, excludedGroups に @Category に渡したクラスを書けば良いです。パッケージを入れた完全修飾名で。複数あるときはカンマで繋げばおk。
で、この書き方はバージョンで差があったりする。

JUnit実践入門では maven-surefire-plugin 2.11 で紹介されており、このバージョンでは以下のような記述になります。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.11</version>
  <dependencies>
    <dependency>
      <groupId>org.amache.maven.surefire</groupId>
      <artifactId>surefire-junit47</artifactId>
      <version>2.11</version>
    </dependency>
  </dependencies>
  <configuration>
    <excludedGroups>net.hogedriven.HogeTests</excludedGroups>
  </configuration>
</plugin>

junit providerを明示的に指定する……となっていますが、これが必要なのは 2.12 までです。maven-surefire-plugin 2.12.1 のおそらく このへん でGroup絡みのJUnitProviderの選択が変更されています。なので以降のバージョンならば dependency などを書かなくても、surefire-junit47 を選択してくれるため、シンプルに書けば良くなっています。
以下はこのエントリを書いた時点での多分最新の 2.14 を使用した場合。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.14</version>
  <configuration>
    <excludedGroups>net.hogedriven.HogeTests</excludedGroups>
  </configuration>
</plugin>

短く書けることはいいことですよね。XMLを手で書く書かないは別にして。

integration-test を Category で扱う

Maven には lifecycle ってのがあり、以下の順で実行されます。*3

validate -> compile -> test -> package -> integration-test -> verify -> install -> deploy

Maven – Introduction to the Build Lifecycle

これらは mvn コマンドの後に続けて書くアレ。よくテストしたい時に `mvn test` とか叩きますよね。順番になってるので test を叩いたときは validate,compile,test の三つが実行される感じ。
で、普段無視されてる*4 integration-test って phase があります。そこで Category わけしたクラスを実行するために maven-failsafe-plugin を使用する。maven-surefire-plugin の実行タイミングを移動させるってのもあるんですが、 surefire を両方で動かすなんてのは出来なさそうだし、integration-test,verify には failsafe を使う感じなので、そっち。
と言うことで net.hogedriven.IntegrationTests カテゴリを test で実行せず integration-tests で実行したい場合、build にこんな感じで書くことになると思います。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.14</version>
  <configuration>
    <excludedGroups>net.hogedriven.IntegrationTests</excludedGroups>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>2.14</version>
  <configuration>
    <groups>net.hogedriven.IntegrationTests</groups>
    <includes>
      <include>**/*Test.java</include>
    </includes>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
    </execution>
  </executions>
</plugin>

気をつける点は……failsafe はデフォルトの対象テストソース名が「**/*IT.java」とかになっているので、カテゴリで扱いたい場合は surefire の命名ルールにあわせてincludeする。でないと名前で振り分けられるので、幾ら XxxTest に IntegrationTests のカテゴリを書いたところで実行してくれない。くらいかな。

Category アノテーションに渡すクラスは何でもいい

ぶっちゃけ何でもいいです。マーク用にインタフェース、例えば SlowTests とかを作るのがよく例に出てますが、これもわかりやすさのため。なので開き直れば java.lang.String とかでいいんです。わかるならね?

動くコード

今回書いた内容で大雑把に書くとこうなります。

Category アノテーションでの単一と複数、pom側でも単一と複数の指定、カテゴリ用クラスには String と Integer を使用しています。
`mvn verify` とか叩いてください。次のような2つのテストログが出ると思います。

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
parallel='none', perCoreThreadCount=true, threadCount=2, useUnlimitedThreads=false
Running IntegerCategoryTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 sec
Running NoneCategoryTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
parallel='none', perCoreThreadCount=true, threadCount=2, useUnlimitedThreads=false
Running IntegerCategoryTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 sec
Running StringAndIntegerCategoryTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec
Running StringCategoryTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

元々のテストクラスは4つで、前者が surefire でカテゴリが java.lang.String を除外したもの。後者が failsafe でカテゴリが java.lang.String と java.lang.Integer を対象としたものになります。

蛇足1

pom.xml の groups に書くクラス名、間違えたり「その時点でクラスパスに通ってないクラス」を指定すると ClassNotFoundException とかなってビルドがずっこけます。凄くダサいです。マーカークラスのリファクタリングとかでクラス名変更やパッケージ移動などを行ったときは注意してください。

蛇足2

この surefire と failsafe の扱いの差よ……

*1:正直記述は微妙。 Category アノテーションのパラメータにクラス指定じゃなく、作ったアノテーションをそのまま付けたい所……。イメージは http://jira.codehaus.org/browse/SUREFIRE-833 とか。

*2:そんなテスト書くなってのは別の話にしておいてください。

*3:だいたい。実際の phase となるともっとあるんだけど、それは http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference でも見てください。

*4:と言うと失礼か……いやでもあまり使ってるの見ない(´・ω・`)