5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-10

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

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

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

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

Concordion とは?

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

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

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

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

/src/test/resource/sample/SplitName.md
# 機能1

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

### 例1

フルネームが "[John Smith](- "#fullname")" のとき、[分割の処理が行われる](- "#result=splitName(#fullname)")と、ファーストネームが "[John](- "?=#result.firstName")" でラストネームが "[Smith](- "?=result.lastName")" となる。

このようなMarkdownを記述します。

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

/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

このように、仕様の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に追加します。

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-javaconcordionの動作に必要なモジュールです。concordionの依存関係にあるparboiled-javaは古いようで java8 ではエラーが出てしまいました。ですのでここで最新版をインストールしております。

3. マークダウンでの仕様の記述

本家サイトのドキュメントにある通り、ここでは仕様を議論しつつ記述していきましょう。

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

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

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

## 前提条件

特になし

### 例

"/hello" をGETでリクエストすると "hello" が返却されるッ。

いや〜、熱い。このビジネスを支える根幹、渾身のAPIです。きっと。

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

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)")では1.での変数#urlを引数として、テストクラスのrequestメソッドを実行し、戻り値を#resultに代入します。
  3. (- "?=#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の流儀の模様です。

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で指定する場合の2パターンについては設定方法を記載しておきます。

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

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

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に設定を追記します。

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に追加します。

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プラグインがおすすめです。

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

concordion api spec HelloAPI html.png

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

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

8. テストクラスの実装

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

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

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

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

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

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";
    }
}

実装が終わったら以下のコマンドで開発サーバーを立ち上げておきましょう。

$ mvn quarkus:dev

10. テストの実施

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

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

concordion api spec HelloAPI html success.png

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

まとめ

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

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

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

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?