PHP製BDDテストフレームワークBehatでE2Eテスト

BehatはPHP製のBDDテストフレームワークです。

概要は下記スライドを見ておくといいでしょう。
http://www.slideshare.net/tchikuba/behatbdd

# 必要なライブラリのインストール

composer.jsonで必要なライブラリをインストールします。

```json:composer.json
{
    "require-dev": {
        "behat/behat": "3.*",
        "behat/mink": "1.*",
        "behat/mink-extension": "*",
        "behat/mink-goutte-driver": "*",
        "behat/mink-sahi-driver": "*",
        "behat/mink-selenium2-driver": "*",
        "behat/mink-zombie-driver": "*"
    }
}
```

## behat/behat
behatの本体です。
テストシナリオを自然言語に近い形で記述し、実行する機能を提供します。

## behat/mink
ブラウザのエミュレーションツールと組み合わせてテストを行う機能を提供します。
behatと組み合わせて利用することが可能です。

## behat/mink-extension
behatで利用できるテストの機能を追加したり、テストケースで扱える言語を拡張したりします。


## behat/mink-*-driver
behat+minkのテストでブラウザのエミュレーションツールを扱えるようにします。
上記サンプルではgoutte、shai、selenium、zombiejsをインストールしていますが、これらは全てインストールする必要はなく利用するブラウザのエミュレーションツールに対応したものをインストールすればいいです。

# behatでテストを行うための準備

behatのテストシナリオを置きたいディレクトリに移動して`vendor/bin/behat --init`コマンドを実行します。
今回は`project/tests`以下にbehatのテストを置いて行くことを前提にします。

コマンドを実行すると以下のようなファイルが配置されます。

```
project/
  tests/
    features/
      bootstrap/
        FeatureContext.php
```

## behat.ymlを用意する

behatでは実行のための設定を`behat.yml`というyaml形式の設定ファイルの記述していきます。
デフォルトではbehatコマンドの実行ディレクトリと同じ階層にある`behat.yml`が読み込まれますが、
`-c`オプションを利用することで`behat.yml`を読み込むパスを指定することも可能です。

今回はプロジェクトディレクトリ直下に`behat.yml`を置くことを想定します。

```yaml:behat.yml
default:
  suites:
    default:
      path: %paths.base%/tests/features
      contexts:
        - FeatureContext
```

### path

behatは、ここで指定したpathのテストシナリオを読み込んでテストを実行するようになります。

### contexts

behatはここで指定したクラスの機能を利用してテストを実行します。

# テストの作成と実行

テストは`behat.yml`のpathに指定したディレクトリ以下に作成していきます。
（今回は`project/tests/features/`以下）
テストファイルの拡張子は`feature`である必要があります。

```empty.feature
# language: ja
フィーチャ: 空のテスト

  シナリオ: 何もしない
```

一行目は利用する言語の指定です。
デフォルトの状態ではテストケースが存在しないため、featureファイルにも何も記述していません。
この状態で`behat`コマンドを実行するとテストが走ります。

```
フィーチャ: 空のテスト

  シナリオ: 何もしない # features\empty.feature:4

No scenarios
No steps
0m0.04s (10.34Mb)
```

## テストで利用可能なケースの確認

behatではテストケースを`behat.yml`のcontext以下に記述したクラスから読み込みます。
利用可能なテストケースは`behat -dl`コマンドで確認出来ます。
デフォルトの状態では何もテストケースが登録されていないため、`behat -dl`コマンドを打っても何も表示されません。

### Mink Extensionのテストケースを追加する

一からテストケースを作っていくのも辛いので、
冒頭でインストールしておいたMink Extensionが提供するテストケースを利用します。

```diff:behat.yml
  default:
    suites:
      default:
        path: %paths.base%/tests/features
        contexts:
          - FeatureContext
+         - Behat\MinkExtension\Context\MinkContext
  extensions:
    Behat\MinkExtension:
      base_url: http://google.com/
```

この状態で`behat -dl`コマンドを実行すると利用可能なテストケースが追加されていることが確認出来ます。

```
default | Given /^(?:|ユーザーは )ホームページを表示している$/
default |  When /^(?:|ユーザーは )ホームページへ移動する$/
default | Given /^(?:|ユーザーは )"(?P<page>[^\s]+)" を表示している$/u
default |  When /^(?:|ユーザーが )"(?P<page>[^\s]+)" へ移動する$/u
default |  When /^(?:|ユーザーが )ページをリロードする$/u
     ・
     ・
     ・
```

## ブラウザのエミュレーションツールと合わせてE2Eテストを行う

googleのトップページから検索を行い、結果画面が表示されることを確認するテストシナリオを作成します。

```googling.feature
# language: ja
フィーチャ: グーグル検索

    シナリオ: recursionを検索
        前提 ホームページを表示している
        もし "q" フィールドに "recursion" と入力する
            かつ "btnK" ボタンをクリックする
        ならば "もしかして: recursion" と表示されていること
```

ブラウザのエミュレーションツールを利用するためには、`behat.yml`を編集する必要があります。

```diff:behat.yml
  default:
    suites:
      default:
        path: %paths.base%/tests/features
        contexts:
          - FeatureContext
          - Behat\MinkExtension\Context\MinkContext
+   extensions:
+     Behat\MinkExtension:
+       base_url: http://google.com/
+       sessions:
+         default:
+           selenium2: ~
```

### base_url

テスト対象のベースとなるURLを指定します。
Mink Extensionが提供するテストケースのうち、ホームページというキーワードはここで指定したURLを指します。

### session

ここに利用するブラウザのエミュレーションツールを指定します。

---

この状態で`behat`コマンドを実行するとテストが走ります。
結果は…

```
フィーチャ: グーグル検索

  シナリオ: recursionを検索                  # features\sample.feature:4
    前提 ホームページを表示している                  # CustomContext::iAmOnHomepage()
    もし "q" フィールドに "recursion" と入力する   # CustomContext::fillField()
    かつ "btnK" ボタンをクリックする              # CustomContext::pressButton()
    ならば "もしかして: recursion" と表示されていること # CustomContext::assertPageContainsText()
      The text "もしかして: recursion" was not found anywhere in the text of the current page. (Behat\Mink\Exception\ResponseTextException)

--- Failed scenarios:

    features\sample.feature:4

1 scenario (1 failed)
4 steps (3 passed, 1 failed)
0m5.97s (11.00Mb)
```

## テストケースを拡張する

先ほどのテストは検索結果の画面が表示される前に、 __"もしかして: recursion" と表示されていること__ というテストケースが走り、テストが失敗してしまいました。

画面が表示されるまで待つ、つまり __n秒待つ__ というテストケースが必要になります。
`bootstrap/FeatureContext.php`を編集して新たにテストケースを加えます。

```php:bootstrap/FeatureContext.php
<?php

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;

class FeatureContext implements Context, SnippetAcceptingContext
{
    public function __construct()
    {
    }

    /**
     * @When :time 秒待つ
     */
    public function wait($time)
    {
        sleep($time);
    }
}
```

```diff:googling.feature
  # language: ja
  フィーチャ: グーグル検索

      シナリオ: recursionを検索
          前提 ホームページを表示している
          もし "q" フィールドに "recursion" と入力する
              かつ "btnK" ボタンをクリックする
+             かつ 1 秒待つ
          ならば "もしかして: recursion" と表示されていること
```

これで`behat　-dl`コマンドを実行するとテストケースが追加されていることが確認出来ます。

```diff
+ default |  When :time 秒待つ
  default | Given /^(?:|ユーザーは )ホームページを表示している$/
  default |  When /^(?:|ユーザーは )ホームページへ移動する$/
  default | Given /^(?:|ユーザーは )"(?P<page>[^\s]+)" を表示している$/u
       ・
       ・
       ・
```

テストを実行すると今度はパスしました。

```
フィーチャ: グーグル検索

  シナリオ: recursionを検索                  # features\sample.feature:4
    前提 ホームページを表示している                  # CustomContext::iAmOnHomepage()
    もし "q" フィールドに "recursion" と入力する   # CustomContext::fillField()
    かつ "btnK" ボタンをクリックする              # CustomContext::pressButton()
    かつ 1 秒待つ                          # FeatureContext::wait()
    ならば "もしかして: recursion" と表示されていること # CustomContext::assertPageContainsText()

1 scenario (1 passed)
5 steps (5 passed)
0m9.29s (10.96Mb)
```

# 所感


* テストシナリオが自然言語っていうのが嬉しい、エクセルテスト表とか書きたくないし、見たくもない。
* seleniumなどのエミュレーションツールを個別に起動しないといけないのが面倒
* behat公式サイトのドキュメントがバージョン2.5のままで、最新版と思って参考にしてしまうと大いにハマる
* テスト１回にかかる時間が長いのでpushやcommit時にテスト実行というやり方はキツイ気がする
* テスト１回にかかる時間が長いのでリリース頻度の高いサービスなどではリリースブランチにマージ前に実行というのもキツイ気がする（テスト実行時間の分リリースが後ろに伸びる、テストがパスしないとさらに長い時間かけてテストするステップを踏むのは辛い）
* テスト１回にかかる時間が長いので定期的に回す程度がいい？（そうすると誰のコミットでおかしくなったか分かりにくい）

実施できるならシステムの品質を保つのにかなり高い効果が上がると思うが、
実際に運用に組み込む場合の懸念が多いのが難点かな…

E2Eテストやってるところはこのあたり、どうやって解決してるんだろう？