SpecTest - ビヘイビア駆動開発(BDD)用テスティング・フレームワーク

# はじめに

[この記事](https://qiita.com/Kray-G/items/30db83922268929e8cd0) の続き。**SpecTest** の内容を少し紹介。まだすぐに使える形ではないですが、要望があれば何とかしたい。...無いか？

気にせずに進みます。こういうのは勢いと思い一気に書いてみました。

ビヘイビア駆動開発では、振る舞いに対するテストを書くことによって動作仕様を明確にしていく。いわゆる **Tests as Documentation**、**Specification by Example** というもの。

しかし既存の BDD フレームワークは「テストを書く＝それがドキュメント」という図式は成り立つものの、ユーザー向けの説明文書になるかというと、決してそうはなっていない、という問題がある、と私は思う、ような気がする。TDD から派生して、あくまで仕様を理解できる、という点にフォーカスしたモノ。

そこで **SpecTest** だ。

*   [SpecTest](https://github.com/Kray-G/SpecTest)
    *   Writing a specification means writing a test, and examples are becoming test codes as is.
    *   訳「仕様を書くことはテストを書くこと、で、例はそのままテストになってるんだよ」

Specification by Example はまさにそんな感じ、Tests as Documentation はなんか逆転して Documentation as Tests みたいな感じだが、BDD というくくりの本質である「**振る舞い駆動**」という意味では間違ってないはず。一応、「今までの BDD テスティング・フレームワークが〇〇な形だから、これは違うよ！」という意見は聞かないことにします...。いや、間違ってないはず。たぶん間違っていないと思う。間違ってないんじゃないかな。ちょっとだけ覚悟しておきます。イメージ的には doctest 系に近いと思うが、よりプログラミングの世界から距離を取っています。書くのはユーザー向けのドキュメント。

恐らく厳密な意味で BDD のこれまでの経緯や定義と照らし合わせると違うとは思うがしかし、理想と現実の間の中で私が欲しいと思うスタイルはこうだった、それは概念として BDD と言って差し支えないだろう、ということは表明しても良いかなー、と思ってます。

まーあまり深く考えないでくださいませませ。

# どんな感じ?

*   プログラミング言語は問わない（なんでも行ける）
*   Markdown で記述
*   `.spectest` に解釈ルールを記載（だが沢山書く必要はない）
*   （Markdown を解釈して自動的にコードを抽出して）テストしてレポート

そう、書くのは仕様。

## プログラミング言語は問わない

作るのはユーザー説明用の Markdown ドキュメントなので、特にプログラミング言語は問わない。あなたの素敵な Ruby で書かれたプロダクトに対しても機能するようにデザインしましたよ。

というか何だったらプログラムじゃなくても Okay だ。`curl` コマンド使って REST API で取ってきた JSON の内容が妥当か、みたいな。

## Markdown で記述

以下がテンプレート。

    # TestSuite Name
    
    最初の `#` で示される表題は自動的にテストスイート名として認識される。
    
    ## なんでも
    
    なんでも書ける。
    
    ## なんでも
    
    なんでも書ける。
    
    ## Examples
    
    `## Examples` はテストコード記述エリア開始のサイン。
    テキスト部分はなんでも書ける。
    
    ### Example 1. TestCase Name
    
    `### Example [0-9]+\\.` はテストケース開始のサイン。
    上記に続く名前がテストケース名になって、1つのテストケースが作られる。
    
    #### Code
    
    `#### Code` はテストコード開始のサイン。
    でも以下のコードブロックがテスト本体。それ以外は何でも書ける。
    
    ```language
    Test Code
    ```
    
    テストコードの結果は標準出力に出すようにすること。
    
    #### Result
    
    `#### Result` はテスト期待値開始のサイン。
    でも以下のコードブロックが期待値本体。それ以外は何でも書ける。
    
    ```
    Expected Result
    ```
    
    ### Example 2. TestCase Name 2
    
    2 つ目のテストケース。以下略。

予め決まったキーワードにさえ気を付けて書けば、テストという意識を持つことなく文書を書ける。キーワードは `.spectest` ファイルで指定も可能。

## `.spectest` に解釈ルールを記載

`.spectest` ファイルのサンプルは以下。詳細は長くなるのでひとまずリンクで... [SpecTest](https://github.com/Kray-G/SpecTest) の表あたりを参照。

```json
{
    "root": "doc/spec",

    "testfile": "test.kx",
    "resultfile": "result.txt",
    "interpreter": "kinx",

    "ignoreFiles": [
        "doc/spec/../benchmark/README.md",
        "doc/spec/spectest/README.md"
    ]
}
```

`root` はドキュメントのあるフォルダ（ディレクトリ）。ここにある `README.md` または `CONTENTS.md` をスタート地点として、そこからリンクされている `.md` ファイルを全てリストアップして実行する。

`testfile` と `resultfile` はテストするときに使用する一時ファイル。毎回上書きして最後に消すので**既にあるファイルを指定しない**こと（要注意）。`interpreter` はテストコードを実行するインタプリタ名。コンパイル言語にはまだ対応していない（そんなに難しくないのでできそうだが）。`ignoreFiles` はリストアップされてしまうファイルのうち、テスト実行対象外にするファイル名を配列で指定しておく。

## テストしてレポート

現在（2020/3/16）の [**Kinx**](https://github.com/Kray-G/kinx) のテスト状況をサンプルにすると、こんな感じになっている。

* 仕様自体は [ここ（Kinx 仕様全体）](https://github.com/Kray-G/kinx/blob/master/docs/spec) です。
    * その中の [declaration 仕様定義 Markdown の例](https://github.com/Kray-G/kinx/blob/master/docs/spec/statement/declaration.md)

```
Test Cout = 69
[<>[<*********>][<****>][<***>][<*>][<*>][<***>][<****>][<******>][<**>][<**>][<***>]
[<****>][<*****>[W]][<*>][<*>][<*>][<*>][W][W][W][W][W][W][W][W][W][W][W][W][W][W][W]
[W][W]]

<Test Result Detail>

Entry: doc/spec/README.md
    Kinx Specification with SpecTest (0.00s)
    Entry: doc/spec/statement/declaration.md
        Declaration statement (0.55s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (With initializer) ...................... successful (  0.06s)
            Case[2] (With initializer of expression) ........ successful (  0.06s)
            Case[3] (Multiple variable declaration) ......... successful (  0.07s)
            Case[4] (Constant value (1)) .................... successful (  0.04s)
            Case[5] (Constant value (2)) .................... successful (  0.06s)
            Case[6] (Constant value (3)) .................... successful (  0.04s)
            Case[7] (Constant value (4)) .................... successful (  0.04s)
            Case[8] (Constant value (5)) .................... successful (  0.08s)
    Entry: doc/spec/statement/enum.md
        Enum statement (0.29s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (With initializer (1)) .................. successful (  0.07s)
            Case[2] (With initializer (2)) .................. successful (  0.07s)
            Case[3] (The scope) ............................. successful (  0.07s)
    Entry: doc/spec/statement/expression.md
        Expression statement (0.23s)
            Case[0] (Assignment) ............................ successful (  0.08s)
            Case[1] (Exponent Evaluation) ................... successful (  0.07s)
            Case[2] (Logical Undefined Operator) ............ successful (  0.06s)
    Entry: doc/spec/statement/mixin.md
        Mixin statement (0.07s)
            Case[0] (Normal case) ........................... successful (  0.07s)
    Entry: doc/spec/statement/block.md
        Block statement (0.08s)
            Case[0] (Scope) ................................. successful (  0.08s)
    Entry: doc/spec/statement/if_else.md
        If-Else statement (0.21s)
            Case[0] (Normal case) ........................... successful (  0.06s)
            Case[1] (No else clause) ........................ successful (  0.06s)
            Case[2] (If-else combination) ................... successful (  0.07s)
    Entry: doc/spec/statement/switch_case.md
        Switch-Case statement (0.31s)
            Case[0] (Normal case) ........................... successful (  0.08s)
            Case[1] (With do-while) ......................... successful (  0.07s)
            Case[2] (Non-integer value) ..................... successful (  0.08s)
            Case[3] (Complex switch-case pattern) ........... successful (  0.07s)
    Entry: doc/spec/statement/try_catch_finally.md
        Try-Catch-Finally statement (0.43s)
            Case[0] (Normal catch) .......................... successful (  0.07s)
            Case[1] (Finally (1)) ........................... successful (  0.07s)
            Case[2] (Finally (2)) ........................... successful (  0.06s)
            Case[3] (Finally (3)) ........................... successful (  0.07s)
            Case[4] (Define own exception) .................. successful (  0.06s)
            Case[5] (Complex example) ....................... successful (  0.07s)
    Entry: doc/spec/statement/while.md
        While statement (0.14s)
            Case[0] (Normal case) ........................... successful (  0.06s)
            Case[1] (Infinaite loop) ........................ successful (  0.07s)
    Entry: doc/spec/statement/do_while.md
        Do-While statement (0.15s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (Infinaite loop) ........................ successful (  0.07s)
    Entry: doc/spec/statement/for.md
        For statement (0.21s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (Infinaite loop) ........................ successful (  0.07s)
            Case[2] (Declation variable in scope) ........... successful (  0.06s)
    Entry: doc/spec/statement/return.md
        Return statement (0.32s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (Without expression) .................... successful (  0.07s)
            Case[2] (if-modifier (1)) ....................... successful (  0.09s)
            Case[3] (if-modifier (2)) ....................... successful (  0.08s)
    Entry: doc/spec/statement/yield.md
        Return statement (0.36s)
            Case[0] (Normal case) ........................... successful (  0.07s)
            Case[1] (Without expression) .................... successful (  0.06s)
            Case[2] (if-modifier (1)) ....................... successful (  0.08s)
            Case[3] (if-modifier (2)) ....................... successful (  0.07s)
            Case[4] (`yield` returns array.) ................ successful (  0.07s)
        Entry(nolink): doc/spec/statement/statement/fiber.md
    Entry: doc/spec/algorithm/qsort.md
        Quicksort (0.08s)
            Case[0] (Quicksort Algorithm) ................... successful (  0.08s)
    Entry: doc/spec/algorithm/heapsort.md
        Heapsort (0.08s)
            Case[0] (Heapsort Algorithm) .................... successful (  0.08s)
    Entry: doc/spec/algorithm/mergesort.md
        Merge Sort (0.08s)
            Case[0] (Merge Sort Algorithm) .................. successful (  0.08s)
    Entry: doc/spec/algorithm/crc32.md
        CRC32 (0.08s)
            Case[0] (CRC32 Algorithm) ....................... successful (  0.08s)
    Entry(nolink): doc/spec/statement/throw.md
    Entry(nolink): doc/spec/statement/function.md
    Entry(nolink): doc/spec/statement/class.md
    Entry(nolink): doc/spec/statement/module.md
    Entry(nolink): doc/spec/statement/lambda.md
    Entry(nolink): doc/spec/statement/closure.md
    Entry(nolink): doc/spec/statement/fiber.md
    Entry(nolink): doc/spec/lib/primitive/integer.md
    Entry(nolink): doc/spec/lib/primitive/double.md
    Entry(nolink): doc/spec/lib/primitive/string.md
    Entry(nolink): doc/spec/lib/primitive/array.md
    Entry(nolink): doc/spec/lib/basic/file.md
    Entry(nolink): doc/spec/lib/basic/directory.md
    Entry(nolink): doc/spec/lib/basic/regex.md
    Entry(nolink): doc/spec/lib/basic/xml.md
    Entry(nolink): doc/spec/lib/basic/zip.md
    Entry(nolink): doc/spec/lib/net/http.md

<Test Result>
    Total Test Cases:       69
        Successful  :       51
        Failed      :        0
        Warning     :       18
```

`Entry(nolink)` はまだドキュメントが書けていないものです。書きます。もちろんテストではなく、仕様（＝例）を。

# おわりに

まだ以下に対応していないので、以下に対応することでもうちょっと実用になると思う。

* Todo
    * JUnit 形式の XML で出力する。多分簡単。
    * [**Kinx**](https://github.com/Kray-G/kinx) をまだ簡単にインストールできないので、簡単にインストールできるようにする。それができないと実質使えない。もしくは完全に Kinx から独立させてしまう。
    * 履歴を保存しておき、テスト結果の推移を可視化できるようにする。
    * CircleCI とかで結果に対するバッチを作れるようにする。
    * コンパイル型のプログラミング言語に対応する。

コンセプトに応援してくださる方は（いつもの通り）★をお願いします。やる気出ると思うので。[**Kinx**](https://github.com/Kray-G/kinx) のほうに。どうしても分離して独立して使えたほうが良いよ、という方（誰に言ってるんだろう...? まぁいいか）は [SpecTest](https://github.com/Kray-G/SpecTest) のほうに★してくれればそういう意思表示と認識しましょう。

* 最初の動機は [スクリプト言語 KINX（ご紹介）](https://qiita.com/Kray-G/items/ca08b6fb40d15dd0ec76) を参照してください。
* リポジトリ
    * リポジトリは [ここ（https://github.com/Kray-G/kinx）](https://github.com/Kray-G/kinx) です。もし宜しければ★をポチっと。
    * SpecTest で独立したリポジトリも用意。[ここ（https://github.com/Kray-G/SpecTest）](https://github.com/Kray-G/SpecTest) です。ただし、現在は [**Kinx**](https://github.com/Kray-G/kinx) へのリンクでしかない。どう独立させるかは検討中。