例えば Quarkus のテストのチュートリアル
Quarkus のテストフレームワークの解説が以下のページにありますが・・・
このチュートリアル、テストの仕方の説明なのに実装ありきなのでイラッときますね。ビヘイビア駆動開発にとって、"実装ありきのテスト"なんてありえないのです。
またBDDやTDDに当たってテストケースを記述する上での問題の一つである**"Mock"する/しない**(卵が先か鶏が先か)という悩みはどういったタイミングで解決していけば良いのでしょうか。
ここではQuarkusのチュートリアルを題材に、"Concordion使ってBDDでやるとどうなるのか?"をシミュレートしてみたいと思います。
Concordion とは?
まず今回ご紹介するConcordionについてですが、Markdown, HTML, Excel などで "仕様" を記述し、その"仕様"の記述に従ってJunit4のテストを実施する、というちょっとアレな変わったフレームワークです。
JavaとC#に対応している模様です。
Getting Start の中身を一部、拝借しまして、例えば以下のような仕様を検討したとします。
フルネームが "John Smith" のとき、分割の処理が行われると、ファーストネームが "John" でラストネームが "Smith" となる。
これを Concordionのスタイルで"Markdown"で書くと以下のように書けます。
# 機能1
フルネームを受け取り、ファーストネームとラストネームを返却する
### 例1
フルネームが "[John Smith](- "#fullname")" のとき、[分割の処理が行われる](- "#result=splitName(#fullname)")と、ファーストネームが "[John](- "?=#result.firstName")" でラストネームが "[Smith](- "?=result.lastName")" となる。
このようなMarkdownを記述します。
これに対応するような以下のテストクラスを作成します。
package sample;
import org.concordion.api.MultiValueResult;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class SplitNameFixture {
public MultiValueResult splitName(String fullname) {
String[] words = fullName.split(" ");
return new MultiValueResult()
.with("firstName", words[0])
.with("lastName", words[1]);
}
}
そして mvn test
などと実行すると、以下のようなテスト結果がHTMLドキュメントとして作成されます。
(画像は本家サイトそのままです。。。)
このように、仕様のMarkdownとそれが満たされているかというHTML形式の検証結果レポート、2つのドキュメントで、プロダクトの仕様の記述とチェックが行えてしまいます。
仕様をテストメソッドのアノテーションに書いたり、"spec"のような特殊な構文で記述しなくても、MarkdownとHTMLという平文の文書(に非常に近い形式)で記述できるのは非常に嬉しいですね。
(Markdownに対応するテストクラスを実装する必要があるので、2度手間にはなります。)
対象環境
それでは、Quarkus プロジェクトで Concordion を導入してみましょう。対象のバージョンなどは以下となります。
Java : JDK 8
Quarkus: 1.1.1
Concordion: 2.2.0
冒頭に挙げたとおり、Quarkus のテストのチュートリアルページを元にプロジェクトを構築していきます。
BDD スタイルでQuarkus REST APIの実装
それでは早速、QuarkusのREST API の実装を BDD スタイルで行っていきましょう。
1. プロジェクトの生成
以下のコマンドでサンプルプロジェクトを生成します。
$ mvn io.quarkus:quarkus-maven-plugin:1.1.1.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=getting-started
cd getting-started
2. テスト用依存関係の追加
以下のモジュールをpom.xml
に追加します。
...
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.concordion</groupId>
<artifactId>concordion</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-java</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
...
まず、quarkus-junit5
はQuarkuJUnitプラグインで、"junit5"と拡張機能が含まれています。rest-assured
は結果のアサート機能付きREST Clientです。
junit-vintage-engine
はQuarkus同梱のJUnit5 上で、Concordion のJUnit4のテストケースを走らせるために必要な、Junitの後方互換エンジンです。
concordion
はConcordionの本体、parboiled-java
はconcordion
の動作に必要なモジュールです。concordion
の依存関係にあるparboiled-java
は古いようで java8 ではエラーが出てしまいました。ですのでここで最新版をインストールしております。
3. マークダウンでの仕様の記述
本家サイトのドキュメントにある通り、ここでは仕様を議論しつつ、記述していきましょう。
数時間に渡る激論の末、以下のようなAPIの仕様が固まったとします。
# Hello API
文字列 `hello` を返却するAPI
## 前提条件
特になし
### 例
"/hello" をGETでリクエストすると "hello" が返却されるッ。
いや〜、熱い。このビジネスを支える根幹、渾身のAPIです。きっと。
そして以下のように "Concordion"スタイルでリンクを設定します。
# Hello API
文字列 `hello` を返却するAPI
## 前提条件
特になし
### [例](- "basic")
"[/hello](- "#url")" をGETで[リクエストする](- "#result=request(#url)")と "[hello](- "?=#result")" が返却されるッ。
Concordion では### 〇〇
の箇所がテストケースに相当し、文中の"#url"、"#result=request(#url)","?=#result"を評価してくれます。
具体的には以下のような評価が行われます。
- (- "#url")では直前の角かっこで括った
/hello
が変数#url
に代入されます。 - (- "#result=request(#url)")では1.での変数
#url
を引数として、テストクラスのrequest
メソッドを実行し、戻り値を#result
に代入します。 - (- "?=#result")では直前の角かっこで括った
hello
が?
に代入され、2.での戻り値#result
と等しいかどうかのアサーションが行われます。
これを複数記述することで複数のケースでテストが実施できます。
ここでは実施しませんが、マークダウンのテーブルを使ったり、Excelでテストケースを定義して実行する、ということも出来るそうです・・・?(というかExcelのサンプルの方がよかったか。。。)
さて、このままでは流石にJUnitは動いてくれませんので、対応するテストクラスを作成いたしましょう。
4. テストクラスの作成
まずは空っぽのテストクラスを作成します。
上記で作成した".md"ファイルと同じパッケージになるように src/test/java
以下にクラスを作成します。
先ほどのパスはsrc/test/resources/com/xxxx/api/HelloAPI.md
でしたので、作成するクラスは**src/test/java/com/xxxx/api/HelloAPIFixture.java
**とします。
サフィックスにFixture
がConcordionの流儀の模様です。
package com.xxxx.api;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
@RunWith(ConcordionRunner.class)
public class FruitsFixture {
}
一旦、これでテストを実行してみましょうか。
5. レポート出力先の変更
テストを実行する前に、レポートの出力先を調整します。
デフォルトではテスト結果のレポートが java.io.tempdir
に出力されます。
これを変更するために concordion.output.dir
を設定する必要があります。
またシステムプロパティはどこからテストを実行するかで設定方法が異なりますので、以下にvscodeで実行した場合と mavenで指定する場合の2パターンについては設定方法を記載しておきます。
5-1. VS Code プラグインの "Java Test Runner" から実行する場合
VS Code プラグインの "Java Test Runner" から実行する場合は、settings.jsonに以下の記述を追記しておきます。
{
...
"java.test.config": {
"name": "concordion",
"workingDirectory": "${workspaceFolder}",
"vmargs" :["-Dconcordion.output.dir=${workspaceFolder}/target/concordion"],
}
...
}
JavaVMの引数として-D
を使っています。
今回はtarget/concordion
以下にレポートが生成されるように設定いたしました。
5-2. mvn test
から実行する場合
mvn
コマンドを使用する場合は以下のようにpom.xml
に設定を追記します。
...
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemProperties>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<concordion.output.dir>target/concordion</concordion.output.dir>
</systemProperties>
</configuration>
</plugin>
...
"maven-surefire-plugin"の設定でsystemProperties
タグの中に使用したいシステムプロパティを設定すればOKです。
6. "maven-surefire-plugin" の調整
maven-surefire-pluginはデフォルトで **/*Test.java
などのそれっぽいファイルをテストクラスとしてピックアップします。
ConcordionではTest
ではなくてFixture
などを接尾辞として使うようですので、これらも含むように設定をpom.xmlに追加します。
...
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*.java</include>
</includes>
<systemProperties>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<concordion.output.dir>target/concordion</concordion.output.dir>
</systemProperties>
</configuration>
</plugin>
...
いろいろアレなので、**/*.java
として全ての.java
ファイルを対象といたしました。。。
7. 実行&レポートの確認
VSCodeで直接、テストクラスをRun Test
しても良いですし、ターミナルからmvn test
を行ってもOKです。
target/concordion/com/xxxx/api/HelloAPI.html
が生成されているはずです。
このHTMLをブラウザから表示してみましょう。
尚、スッピンのHTMLなどを使用する際には個人的には VSCode の Live Serverプラグインがおすすめです。
さてブラウザからレポートを確認してみましょう。
一旦はこのように、"メソッドがない"というエラーが出ます。
それではテストメソッドを実装していきましょう。
8. テストクラスの実装
以下のようにテストクラスのメソッドを実装しましょう。
package com.xxxx.api;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;
import static io.restassured.RestAssured.given;
@RunWith(ConcordionRunner.class)
public class FruitsFixture {
public String request(String path) {
return given()
.when().get(path)
.body().asString();
}
}
Quarkusのテストフレームワークからgiven
を拝借して使用致しました。
これでHelloAPI.mdで定義した"例"の箇所のFixtureがちゃんと動くはずです。
今回はAPIという"外側からの仕様"をマークダウンで記述しました。
それではDBにアクセスしてビジネスロジックを記述するサービス層の仕様をMarkdownで記述したとすると、どうでしょう。そこではMockが必要になるはずです。
ではDBにアクセスする永続化層の仕様をMarkdownで記述したとすると、どうでしょう。
Mockではなく実際のDBのアクセスが必要になるでしょう。
このようにテストする層を絞ることでモックがいるかいらないか、が変わってきます。
今回は"外部の仕様をテスト"するのでquarkus:dev
でWEBサーバーの起動が必要になってきます。本来であればDBとの接続もできていなければならないはずです。
このようにテストが何の層(レイヤー)をスコープとするか、で必要なコンポーネントが変わってくると思います。
それでは再度、テストを実行してみましょう。
ちゃんと(!)失敗したはずです。いよいよ、機能の実装に移りたいと思います。
9. APIの実装とサーバーの起動
それでは失敗ケースを成功ケースに変えるべく、APIクラスを実装しましょうか。
テストケースでパスや戻り値が指定されているので迷うことなく実装できるはずです。
package com.xxxx.api;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/hello")
public class HelloResoruce {
@GET
public String get() {
return "hello";
}
}
実装が終わったら以下のコマンドで開発サーバーを立ち上げておきましょう。
$ mvn quarkus:dev
10. テストの実施
それでは実装したAPIがちゃんと仕様を満たしているか確認をしましょう。
再度、テストを実施してみてください。
実施後、レポートを確認すると・・・
無事にHTMLのレポートが"グリーン"に変わったかと思います!
これで仕様が満たされていることが確認できました。
まとめ
Markdownのような文書で要求や仕様が記述できて、同時にテストケースのFixtureを定義して、そのままテストクラスに流し込めて結果レポートまで出してくれるなんて、なかなか素晴らしい仕組みだと思います。
Concordion初めて触ってみたけどなかなか興味深いですね!
今回は以上といたします。