Mavenで結合テストを自動化する方法

こんにちは、九岡です。
Javaエンジニアのみなさん、結合テストの自動化してますか?!

この記事では、

  • 結合テストとは何か
  • 筆者は何のために行っているのか
  • それをMavenで自動化する方法

をご紹介します。

用途が知られていたりいなかったり、単体テストに比べると情報が少なかったり、より多くのMavenプラグインを使うことになりがちで手間がかかる「結合テストの自動化」。
「まだやってない」という方は、この記事をとっかかりにしていただけるとうれしいです!

対象

この記事は特に以下のような方におすすめです。

  • JavaやMavenを利用してアプリケーション開発を行っている方
  • テスト自動化をはじめて行う方
  • 単体テストは自動化しているが、結合テストはまだ自動化していないという方
  • 自分でMavenのビルド設定ができるようになりたい方

既にJavaプロジェクトで結合テストを自動化している方にとっては目新しいことはないかもしれません。しかし、例えばチームに配属される新しいメンバーにこの記事を読んでいただいて、pom.xml(秘伝のタレ)を理解する取っ掛かりにしていただく、というような使い方はできるかもしれません。

また、Seleniumで機能テストや受け入れテストを自動化したり、例えばGradleのようなMaven以外のビルドツールを使う場合の方法は、本記事の対象外です。

用語

Java界隈に詳しくない方でもイメージがつかめるように、基本的な用語をまとめておきます。

Maven
主にJavaを対象としたビルドツール

TestNG
Javaのテストフレームワークの一つ

Mavenプラグイン
Mavenの機能はプラグインで拡張できるようになっています。結合テスト実行などの機能もプラグインで実現されています

Solr
Javaで実装された全文検索を行うためのWebアプリケーション。結合テストの説明にサンプルとして登場します

Mavenプラグイン

この記事では、特に以下のようなMavenプラグインを使います。

  • Cargoプラグイン
  • Downloadプラグイン
  • MySQLプラグイン
  • Failsafeプラグイン
  • Build Helperプラグイン

考え方

より知られていそうな「単体テスト」と比較しながら、この記事でいうところの「結合テスト」についてご説明します。

なお、それぞれのテストの意味・実態が人や組織によって異なることが想像されます(実際、単体テストという名目でされていたテストが、筆者の解釈だと結合テストに該当する、といったこともありました)。
そのため、特に意見が割れそうなところは「筆者の場合」のような前置きを入れるなどして、少し表現をぼかしています。
皆さんの身近なところでの意味と照らし合わせながらお楽しみください!

単体テスト

単体テストでは、あるモジュール単体の機能をテストします。
オブジェクト指向言語を採用しているなら、あるクラス単体の機能をテストすることです。JavaにおいてはJUnitやTestNGなどのテストフレームワークを使って行われます。
筆者の場合、

  • ある条件(コンストラクタ引数・メソッド引数・フィールドなど)のとき、対象クラスのメソッドが
    • 返す値が仕様通りか
    • 起こす副作用が仕様通りか

といったテストをします。

結合テスト

一方で、今回注目する結合テストでは、複数のモジュールを組み合わせたときの機能をテストします。
大雑把な説明ですが、「単体テストはあるモジュール単体を対象とするのに対して、結合テストは複数のモジュールの組合せを対象とする」と考えてください。
その性格上、結合テストではいくつかのクラス、時にファイルシステム、データベース、Webサービスなどまでが組み合わされることがあります。
筆者の場合、機能テスト(結合テストの一種)ではコストパフォーマンスがよくない・カバーしきれない、かつ単体テストだけでは不安な場合に、特に気になるところを結合テストでカバーします。

例えば、個人的によくあるケースでは、結合テストで以下のようなことを確認します。

  • あるオブジェクトの組み合わせでメソッドを呼び出した場合に、
    • DBのテーブルに追加されたレコードが正しいか
    • 新規保存されたファイルの内容は正しいか
    • 送信されるメールの内容は正しいか
  • DBのテーブルがある状態のとき
    • あるオブジェクトの組み合わせで検索メソッドを呼び出した時に、検索結果が正しいか
  • など

筆者の場合、テスト内容はこのように機能テストに近いことが多いです。

この手の結合テストを行う利点は、Javaで実装するようなアプリでいうと、

  • 特定のモジュールの組み合わせ
  • 設定ファイルの内容や場所
  • ミドルウェアの種類やバージョン、利用方法
  • 利用するWebサービスの利用方法
  • JVMの起動オプションやシステムプロパティなど

などが原因で動作しないコードを発見し、改善する機会が得られることです。また、十分にレガシーで仕様がよくわからないシステムなどに対してまず結合テストを実施することで、そのシステムがどのような条件や環境で動くのかを明らかにする、というような使い方もできます。

結合テストにはいくつか難しい点がありますが、そのうち一つは「組み合わせるモジュールが多くなるとそのセットアップだけで手間がかかる」ということです。
例えば、開発中のWebアプリケーションの仕様に「GlassfishやJettyで動作する」というものがあるとします。
これをテストに落としこむと「Webアプリケーションと2つの異なるサーブレットコンテナとを組み合わせた時に機能するかどうかの結合テスト」が考えられます。
このような結合テストを行うためには、組み合わせ毎に以下のようなタスクが必要になります。

  • サーブレットコンテナのインストール
  • サーブレットコンテナの設定
  • サーブレットコンテナの起動
  • Webアプリケーションのデプロイ
  • テスト実行
  • (テストの失敗・成功にかかわらず)サーブレットコンテナの停止

テストのたびにこのようなセットアップを手動にしろ自動にしろ行うのは手間がかかります。
その手間を最小化する一つの方法として、本記事ではMavenとMavenプラグインを使います。

Mavenプラグイン

単体テスト実行や結合テスト実行などの機能はMavenに組み込まれておらず、プラグインで実現されています。
前述のとおり、結合テストに関するセットアップの手間を軽減するために使えるプラグインもあります。
そんなプラグインのうち、この記事で利用するプラグインをご紹介します。

Failsafeプラグイン

Maven Failsafe Plugin - Introduction

Mavenから結合テストを実行するためのプラグインです。
単体テストのためのSurefireプラグインと似ていますが、主な違いは「結合テストの後始末」が行える点です。
Failsafeプラグインは

  • テスト実行前にpre-integration-test
  • テスト実行後にpost-integration-test

というphaseを必ず実行してくれます。それぞれのphaseでMavenに実行させるタスクはカスタマイズが可能です。そして、post-integration-testは結合テストが失敗した場合でも実行されます。そのため、典型的には前者で結合テストの環境を用意し、後者でその環境を破棄します。

Surefireプラグインを使う場合と同様に、テストそのものはJUnitやTestNGなどのテストフレームワークを使って書きます。

Cargoプラグイン

Cargo - Maven2 plugin

CargoはTomcatやJettyなどの様々なServletコンテナの操作を自動化するためのラッパとなるJavaライブラリです。
それをMavenから簡単に、pom.xmlの設定だけで利用できるようにしてくれるのがCargoプラグインです。
これを利用すると、Servletコンテナ上で実行するようなWebアプリケーションと連携した結合テストも自動化できます。
「Servletコンテナ上で実行するようなWebアプリケーション」ときいてピンとこない方もいらっしゃるかもしれませんが、例えば全文検索サービスを提供するSolrや、GitHubクローンのGitBucketなどもそうです。

MySQLプラグイン

jcabi-mysql-maven-plugin - MySQL Maven Plugin for Integration Testing

MySQLプラグインは、結合テスト用にローカルのMySQLをセットアップしてくれるプラグインです。

結合テストでMySQLが必要な場合、みなさんならどうしますか?
例えば、テストを実行する人が手動で対応することにして、READMEなどに「結合テスト実行前にローカルマシンにMySQLをインストールしてください」のような案内を入れる、というのもひとつの方法です。
しかし、MySQLはOS(WindowsやLinux、Mac OS X)やCPUアーキテクチャ(32bit、64bit)など毎にバイナリ形式で配布されています。
それをダウンロード、インストールして、基本的な設定とディレクトリをセットアップして、結合テストが始まる前にMySQLを起動し、成功したか失敗したかにかかわらず結合テスト後には停止する…などと考えだすと、かなりの煩雑さになりそうです。
これらをpom.xmlの設定だけで自動化してくれるのがMySQLプラグインです。

Downloadプラグイン

maven-download-plugin/maven-download-plugin

Mavenによるビルドの一環としてファイルをダウンロードするためのプラグインです。

このプラグインはどのような場合に使えるのでしょうか?

例えば、結合テスト時に開発対象とは異なる別のアプリケーションを起動する必要があるとします。
起動する前に最低限それをインストールしておく必要があります。その方法として、予めそのアプリケーションを手動でインストールしてもらう、何らかのツールで自動化する、といったことが考えられます。
自動化するにしても、Mavenで完結できればそれに越したことはありません。
そこでDownloadプラグインの出番です。
このプラグインには、Mavenに任意のファイルをダウンロード・キャッシュ・展開させる機能があります。
この機能を使えば、インストール手順が「アーカイブをダウンロードして展開するだけ」のようなアプリをインストールする分には十分です。

Build Helperプラグイン

Build Helper Maven Plugin - Introduction

単体テスト・結合テスト・その他Mavenで行うタスクでよく使われるようなユーティリティ的な機能を集めたプラグインです。
Mavenのプロパティを動的に書き換えたりするなどの機能もありますが、この記事では「空いているポート」を確保するために使います
例えば、MySQLとの結合テストを行う場合、結合テスト中だけMySQLを起動したくなりますが、そのときにデフォルトの3306を使ったりすると、環境によっては既に起動しているMySQLのポートと被ってしまいそうです。
被るとテストに支障があるので、現在使われていないポートを自動的に確保したいところです。
このような「空いているポートの自動確保」などの昨日がBuild Helperプラグインには含まれています。

実践

Surefireプラグインによるテスト

Failsafeプラグインで結合テストを行う場合でも、テストケースはSurefireプラグイン同様JUnitなどのテストフレームワークを使って書きます。
まずはテストケースの書き方をおさらいするために、SurefireとTestNGで単体テストを実行してみましょう。

Mavenではpom.xmlファイルにビルド設定を記述します。ビルド設定には利用するJavaライブラリ・Mavenプラグインやその設定などが含まれます。
Surefireプラグインを使って単体テストを行う場合のpom.xmlの一例は以下のとおりです。特にdependencies要素とbuild要素の中に注目してください。

pom.xml

dependencies内ではTestNGのライブラリ依存性を追加して、build>plugins内ではmaven-surefire-pluginの依存性を追加しています。

次に、テストに集中するためにシンプルなテスト対象クラスを実装してみます。整数値の足し算をするaddメソッドを持つだけのクラスです。

Calculator.java

このクラスに対するテストを一つ実装してみます。
先ほどのCalculatorのaddメソッドが1+2=3を計算できることをテストします。TestNGが提供するassertEqualsメソッドの第1引数が期待結果で、第2引数が実結果です。

CalculatorTest.java

mvn testコマンドでpom.xmlの内容に応じたプロジェクトのビルドと単体テストの実行が行われます。

以下のような出力がされれば、テスト成功です。
「Tests run: 1」のとおり1件のテストが実行され、「Failures: 0, Errors: 0, Skipped: 0」のとおり1件も失敗・エラー・スキップがないのでOKです。

Failsafeプラグインによるテスト

次はFailsafeプラグインを使って、結合テストを実行してみましょう。Surefireプラグインを使う場合とよく似ているので、そちらの導入に成功しているのであれば、すんなりいけると思います!

まず、pom.xmlにmaven-failsafe-pluginを追加します。

pom.xml

Surefireプラグインを利用する場合との違いはartifactIdとexecutionsのみです。artifactIdはmaven-surefire-pluginの代わりにmaven-failsafe-pluginにします。そして、executions要素が追加されます。executions要素には結合テスト実行時に実行するSurefireプラグインのゴールを指定します。今回は単純にintegration-testとverifyが1回ずつでOKです。例えば結合テストを1回実行するたびに異なる設定でintegration-testを複数回実行したい、というような場合は変更が必要です。詳しくはSurefireプラグインの使い方ページ(英語)をご覧ください。

結合テストクラスは◯◯ITという名前で作成します。あくまで例として、先ほどの単体テストと同じ内容のテストを実行してみましょう。違いはクラス名とファイル名だけです。クラス名は単体テストのCalculatorTestに対して結合テストではCalculatorITにしましょう。このテスト自体は全く結合テストになっていませんが、SurefireプラグインとFailsafeプラグインの使い方の違いをご説明することに集中するためあえてこのようにします。

CalculatorIT.java

結合テストはmvn verifyコマンドで実行します。
実行すると、以下のようにテストが1件通るはずです。

念のため補足すると、mvn testではテストクラス名の違いにより、この◯◯ITは実行されません。
以上がFailsafeプラグインの基本的な使い方です。
以降では、Failsafeプラグインの利用を前提として、より複雑な結合テストについてご説明します。

MySQLとの結合テスト

さて、このあたりから事例が比較的少ないパートに入っていきます。まずはMySQLとの結合テストです。

データベースにアクセスするようなクラスがあるとして、それを実際にMySQLへ接続させてうまく動作するかをテストします。

MySQLを起動するときはMySQLがLISTENするポート番号を指定したいところですが、例えばデフォルトの3306番などにしてしまうのはどうでしょうか?
今回の結合テストとは関係なく、別の用途で既にMySQLを起動していたとしたらどうでしょう。もしポートが重複していたら、結合テスト用のMySQLが起動できません。

結合テスト用のMySQLに使えるような重複しないポート番号を自動的に採番したくなりますね。そのようなポート番号の採番にはBuilder Helper Mavenプラグインが利用できます。

サンプルのpom.xmlをGitHub上でご確認ください。

Builder Helper Mavenプラグインのreserve-network-portゴールによって、mysql.portというMavenプロパティにMySQLのポート番号を確保しています。

確保されたポート番号はMySQLプラグインに渡されています。${mysql.port}でMavenプロパティが展開されます。展開先はMySQLプラグインのconfiguration > port要素です。configuration > port要素はMySQLプラグインの設定項目で、プラグインはここに指定されたポート番号でMySQLを起動します。

次にこのポート番号を実際のテスト時に参照する必要がありますが、そのためにはJavaのシステムプロパティを経由します。テスト時にJavaのシステムプロパティを変更するためには、Failsafeプラグインの設定のうちsystemPropertyVariables要素を使います。ここにシステムプロパティと同じ名前の要素を作成し、先ほどと同様に${mysql.port}で展開したMavenプロパティの値を渡してやることで、結果的にテストクラスからシステムプロパティ経由でポート番号を知ることができます。

テストクラスでは、普通にSystem.getPropertyを利用して以下のようにシステムプロパティを参照します。

その他、DBへの接続方法などテストの詳細についてはこの記事の範囲外なので省略させていただきます。サンプルコードはGitHubでご覧ください。ORMなどの利用は避けて、結合テストの実装方法をお見せすることに注力したコードにしました。

結合テストを実行するためには、前述のとおりmvn verifyを実行します。

MySQL Mavenプラグインの機能は「指定されたOSやCPUアーキテクチャに対応するMySQLをセットアップする」というものでした。これを指定するためには、mvn verifyコマンドを実行する際、mysql.classifierというシステムプロパティを設定します。有効なclassifierの値については、Mavenセントラルレポジトリにアップロードされているファイルが参考になります。例えば、64-bit kernelを使う最近のMacであれば、mac-x86_64を指定します。その場合、コマンドは以下のようになります。

このコマンドを実行すると、まずBuild HelperプラグインによりMySQL用のポートが確保されます。

次にJCabi MySQLプラグインがMySQLをインストール・起動します。以下では自動的にインストールされたMySQLが起動して、--port=確保したポート番号のオプションによってポートが指定されています。ちゃんと確保したポートがJCabi MySQLプラグインに渡されていますね。

最終的に、先ほど作成したテストが通っていれば成功です。

以上でMySQLを使う結合テストが自動化できるようになりました。
最後にWebアプリケーションとの結合テストについてご説明します。ここまでできれば、大抵の結合テストは自動化できる基礎力がついていると思います。

Webアプリケーションとの結合テスト

いよいよ最後です!
ServletコンテナへデプロイするWebアプリケーションとの結合テストを行ってみましょう。
Webアプリケーションといっても、プロジェクト内で開発しているものや、OSSとしてzipアーカイブなどの形で配布されているものなど色々あります。
前者はネット上にも情報が多いので、今回はよりマイナーな後者についてご説明します。
後者の一例としてSolrとの結合テストをしてみましょう。

この記事の冒頭の用語集で軽くご説明したとおり、Solrは全文検索サービスを提供するWebアプリケーションです。TomcatやJettyなどのサーブレットコンテナにデプロイして動作させます。たとえば皆さんが開発するアプリケーションがSolrの全文検索機能を利用しているとして、結合テストをするためにはテストのたびにSolrをセットアップする必要があります。事前にセットアップしておいたり、TomcatやJettyなどのAPIをJavaから呼び出して自動化することも可能かもしれません。しかし、Cargoプラグインを利用すると比較的簡単に実現できます。

早速、pom.xmlをざっと眺めてみましょう。
長いので、今回もGitHub上でご確認ください

本当に長くなってきましたね!
基本的には、リファレンスガイドの通り設定しますが、これをひと通り読むのは骨が折れるため、個別にご説明していきます。

dependenciesではTestNGとSolrjへのライブラリ依存性を追加しています。SolrjはSolrのクライアントライブラリで、結合テストにおいて実際にSolrへ接続するための利用します。

次にbuildを見てみましょう。FailsafeプラグインのほかにDownloadプラグインとCargoプラグインの設定をしています。

download-maven-pluginにはapache.orgで配布されているSolrのzipアーカイブをダウンロード・展開する設定をしています。ポイントは以下の部分です。

zipアーカイブをダウンロードするために、url要素にzipアーカイブへのURL、md5要素にzipアーカイブのMD5ハッシュを指定しています。いずれもSolrの配布場所で調べられます。さらに、ダウンロードしたzipアーカイブを展開する必要があります。そのためにはunpack要素にtrue、output-directory要素に展開先ディレクトリへのパスを指定します。

cargo-maven2-pluginには、Jettyを起動してSolrのWebアプリケーションをデプロイする設定をしています。

まずは利用するServletコンテナを指定します。
今回はJetty 8.1.14を使います。利用できるJettyのバージョンについてはMavenセントラルにアップロードされているアーティファクトの情報などが参考になります。

次はサーブレットコンテナの設定です。

まず、typeによってCargoがどのようにServletコンテナをセットアップするかを指定します。typeの説明はCargoのドキュメントにあります。standaloneは指定したディレクトリ以下に必要な設定ファイルなどを都度生成するという方法です。その他には設定済みのディレクトリを使うexistingや、既に起動しているServletコンテナを使うruntimeがあります。今回はstandaloneを使います。

homeにはServletコンテナの設定が書き出される先となるディレクトリへのパスを指定します。

propertiesにはCargoのドキュメントに記載されている項目が設定できますが、今回はServletコンテナのポートを8983に設定するためにcargo.servlet.portの設定を行っている点がポイントです。
今回は簡単のためSolrへのポート番号は8983で固定としますが、実際にはMySQL同様、Builder Helperプラグインでポートを確保したほうがよいでしょう。

最後に、Downloadプラグインでダウンロード・展開したSolrをデプロイする設定です。

zipアーカイブにはSolrのWebアプリケーションのwarファイルが同梱されているため、それをデプロイすることにします。
warファイルをデプロイする場合は、configurationのtype要素に「war」を指定します。
そしてlocation要素にSolrのwarファイルへのパス、properties要素以下のcontext要素にコンテキストパスを指定します。

以上でpom.xmlは完成です。

テストケースはGitHub上でご覧ください

結合テストを実行するためには、これまでと同様にmvn verifyコマンドを使います。

ログを追ってみましょう。コマンドを実行すると、まずDownloadプラグインによりSolrのzipアーカイブがダウンロードされます。初回はこのダウンロードのためしばらく時間がかかります。2回め以降はキャッシュされるので、待ち時間は数秒です。

次にCargoプラグインが実行されます。このときJettyが起動してSolrがデプロイされます。

そしてFailsafeプラグインによって結合テストが実行されます。

テストの成功・失敗にかかわらず、最後に再度Cargoプラグインが実行されます。このときJettyが停止します。

最終的に、以下のようにテストが成功していればOKです。おつかれさまでした!

まとめ

この記事では、

  • 結合テストとは何か
  • 筆者は何のために行っているのか
  • それをMavenで自動化する方法

をご紹介しました。

結合テストは用途が知られていたりいなかったり、単体テストに比べると情報が少なかったり、手間がかかるために自動化が後回しになりがちです。
この記事が皆さんのプロジェクトで結合テスト自動化の取っ掛かりとして活用されれば嬉しいです!