【2020年1月版】世のチュートリアルは実装先行でイラッとする。もしくは Concordion で BDDに入門する。

# 例えば Quarkus のテストのチュートリアル

Quarkus のテストフレームワークの解説が以下のページにありますが・・・

- [QUARKUS - TESTING YOUR APPLICATION](https://quarkus.io/guides/getting-started-testing)

このチュートリアル、テストの仕方の説明なのに**実装ありき**なのでイラッときますね。ビヘイビア駆動開発にとって、"実装ありきのテスト"なんてありえないのです。
またBDDやTDDに当たってテストケースを記述する上での問題の一つである**"Mock"する／しない**(卵が先か鶏が先か)という悩みはどういったタイミングで解決していけば良いのでしょうか。

ここではQuarkusのチュートリアルを題材に、"Concordion使ってBDDでやるとどうなるのか？"をシミュレートしてみたいと思います。

## Concordion とは？

まず今回ご紹介するConcordionについてですが、Markdown, HTML, Excel などで "仕様" を記述し、その"仕様"の記述に従って**Junit4のテストを実施する**、というちょっと~~アレな~~変わったフレームワークです。
JavaとC#に対応している模様です。

- [公式サイト](https://concordion.org/)
- [公式サイト ドキュメント](https://concordion.org/discussing/java/markdown/)

Getting Start の中身を一部、拝借しまして、例えば以下のような仕様を検討したとします。

> フルネームが "John Smith" のとき、分割の処理が行われると、ファーストネームが "John" でラストネームが "Smith" となる。

これを Concordionのスタイルで"Markdown"で書くと以下のように書けます。

```md:/src/test/resource/sample/SplitName.md
# 機能１

フルネームを受け取り、ファーストネームとラストネームを返却する

### 例１

フルネームが "[John Smith](- "#fullname")" のとき、[分割の処理が行われる](- "#result=splitName(#fullname)")と、ファーストネームが "[John](- "?=#result.firstName")" でラストネームが "[Smith](- "?=result.lastName")" となる。
```
このようなMarkdownを記述します。

これに対応するような以下のテストクラスを作成します。

```java:/src/test/java/sample/SplitNameFixture.java
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ドキュメントとして作成されます。
(画像は本家サイトそのままです。。。)
![tutorial-successful.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/0f0accbd-32f4-097b-7038-4b7456419fcf.png)

このように、仕様のMarkdownとそれが満たされているかというHTML形式の検証結果レポート、２つのドキュメントで、プロダクトの仕様の記述とチェックが行えてしまいます。

仕様をテストメソッドのアノテーションに書いたり、"spec"のような特殊な構文で記述しなくても、MarkdownとHTMLという平文の文書(に非常に近い形式)で記述できるのは非常に嬉しいですね。
~~(Markdownに対応するテストクラスを実装する必要があるので、２度手間にはなります。)~~

## 対象環境

それでは、Quarkus プロジェクトで Concordion を導入してみましょう。対象のバージョンなどは以下となります。

Java : JDK 8
Quarkus: 1.1.1
Concordion: 2.2.0

冒頭に挙げたとおり、Quarkus のテストのチュートリアルページを元にプロジェクトを構築していきます。

- [QUARKUS - TESTING YOUR APPLICATION](https://quarkus.io/guides/getting-started-testing)

## BDD スタイルでQuarkus REST APIの実装

それでは早速、QuarkusのREST API の実装を BDD スタイルで行っていきましょう。

### 1. プロジェクトの生成

以下のコマンドでサンプルプロジェクトを生成します。

```shell
$ mvn io.quarkus:quarkus-maven-plugin:1.1.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=getting-started
cd getting-started
```

### 2. テスト用依存関係の追加

以下のモジュールを`pom.xml`に追加します。

```xml: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. マークダウンでの仕様の記述

本家サイトのドキュメントにある通り、ここでは仕様を[議論しつつ](https://concordion.org/discussing/java/markdown/)、[記述して](https://concordion.org/documenting/java/markdown/)いきましょう。

数時間に渡る激論の末、以下のようなAPIの仕様が固まったとします。

```markdown:src/test/resources/com/xxxx/api/HelloAPI.md
# Hello API

文字列 `hello` を返却するAPI

## 前提条件

特になし

### 例

"/hello" をGETでリクエストすると "hello" が返却されるッ。
```
いや〜、熱い。**このビジネスを支える根幹、渾身のAPI**です。きっと。

そして以下のように "Concordion"スタイルでリンクを設定します。

```markdown:src/test/resources/com/xxxx/api/HelloAPI.md
# Hello API

文字列 `hello` を返却するAPI

## 前提条件

特になし

### [例](- "basic")

"[/hello](- "#url")" をGETで[リクエストする](- "#result=request(#url)")と "[hello](- "?=#result")" が返却されるッ。
```
Concordion では`### 〇〇`の箇所がテストケースに相当し、文中の"#url"、"#result=request(#url)","?=#result"を評価してくれます。

具体的には以下のような評価が行われます。

1. (- "#url")では直前の角かっこで括った`/hello`が変数`#url`に代入されます。
2. (- "#result=request(#url)")では１.での変数`#url`を引数として、テストクラスの`request`メソッドを実行し、戻り値を`#result`に代入します。
3. (- "?=#result")では直前の角かっこで括った`hello`が`?`に代入され、2.での戻り値`#result`と等しいかどうかのアサーションが行われます。

これを複数記述することで複数のケースでテストが実施できます。
ここでは実施しませんが、[マークダウンのテーブル](https://concordion.org/instrumenting/java/markdown/)を使ったり、[Excelでテストケースを定義](https://github.com/concordion/concordion-excel-extension/blob/master/README.md)して実行する、ということも出来るそうです・・・？(というか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の流儀の模様です。

```java:src/test/java/com/xxxx/api/HelloAPIFixture.java
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で指定する場合の２パターンについては設定方法を記載しておきます。

#### 5-1. VS Code プラグインの "Java Test Runner" から実行する場合

VS Code プラグインの "Java Test Runner" から実行する場合は、settings.jsonに以下の記述を追記しておきます。

```json:settings.json
{
...
    "java.test.config": {
        "name": "concordion",
        "workingDirectory": "${workspaceFolder}",
        "vmargs" :["-Dconcordion.output.dir=${workspaceFolder}/target/concordion"],
    }
...
}
```
JavaVMの引数として`-D`を使っています。
今回は`target/concordion`以下にレポートが生成されるように設定いたしました。

- [Run with Configuration | microsoft/vscode-java-test](https://github.com/Microsoft/vscode-java-test/wiki/Run-with-Configuration)

#### 5-2. `mvn test` から実行する場合

`mvn`コマンドを使用する場合は以下のように`pom.xml`に設定を追記します。

```xml: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です。

- [Using System Properties | Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/examples/system-properties.html)

### 6. "maven-surefire-plugin" の調整

maven-surefire-pluginはデフォルトで `**/*Test.java`などの**それっぽいファイル**をテストクラスとしてピックアップします。

- [Inclusions and Exclusions of Tests](https://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html)

Concordionでは`Test`ではなくて`Fixture`などを接尾辞として使うようですので、これらも含むように設定をpom.xmlに追加します。

```xml: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プラグイン](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)がおすすめです。

さてブラウザからレポートを確認してみましょう。

![concordion api spec HelloAPI html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e8d82504-26ee-c25a-0d56-e50da3fa51b6.png)

一旦はこのように、"メソッドがない"というエラーが出ます。

それではテストメソッドを実装していきましょう。

### 8. テストクラスの実装

以下のようにテストクラスのメソッドを実装しましょう。

```java:src/test/java/com/xxxx/api/HelloAPIFixture.java
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との接続もできていなければならないはずです。

このようにテストが何の層(レイヤー)をスコープとするか、で必要なコンポーネントが変わってくると思います。

それでは再度、テストを実行してみましょう。

![concordion api spec HelloAPI html error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/98c0b6d1-cb55-93ff-a2b3-ef3acabad810.png)

ちゃんと(！)失敗したはずです。いよいよ、機能の実装に移りたいと思います。

### 9. APIの実装とサーバーの起動

それでは失敗ケースを成功ケースに変えるべく、APIクラスを実装しましょうか。
テストケースでパスや戻り値が指定されているので迷うことなく実装できるはずです。

```java:src/main/java/com/xxxx/api/HelloResource.java
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";
    }
}
```
実装が終わったら以下のコマンドで開発サーバーを立ち上げておきましょう。

```shell
$ mvn quarkus:dev
```

### 10. テストの実施

それでは実装したAPIがちゃんと仕様を満たしているか確認をしましょう。
再度、テストを実施してみてください。

実施後、レポートを確認すると・・・

![concordion api spec HelloAPI html success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/7c2955bc-5c33-b4f3-e7de-683db15d6d4c.png)

無事にHTMLのレポートが"グリーン"に変わったかと思います！
これで仕様が満たされていることが確認できました。

## まとめ

Markdownのような文書で要求や仕様が記述できて、同時にテストケースのFixtureを定義して、そのままテストクラスに流し込めて結果レポートまで出してくれるなんて、**なかなか素晴らしい**仕組みだと思います。

Concordion初めて触ってみたけどなかなか興味深いですね！

今回は以上といたします。