CentOS7 + behat3 + Selenium2導入まとめ

## はじめに
プロジェクト内で機能追加をする際、デグレの調査がかなりめんどくさいと思ったのと、
毎回同じテストを繰り返す時間がもったいなく感じたので、自動テストを導入しました。

今回は、CentOS7でbehat3 + Selenium2でやってみたのでまとめました。

## 環境
* Windows(8.1、10で確認)
* Vagrant 1.9.7
* VirtualBox 5.1.~
* CentOS7.3
* PHP7.1(今回は7.1で実施しました。)

## 事前準備
必要なパッケージをインストールします。

* firefox
`yum -y install firefox`

* 仮想ディスプレイを作る
`yum -y install xorg-x11-server-Xvfb`
仮想ディスプレイの出力先も同時に設定しましょう。
`vi ~/.bash_profile`で以下を記載します。
`export DISPLAY=:1`
編集後は再読み込みさせましょう。
`source ~/.bash_profile`

### Javaのインストール
Selenium2を動作させるにはJavaをインストールする必要があるため、Javaもインストールしましょう。
`yum -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel`

### Selenium本体をダウンロード
Selenium本体をダウンロードします。
SeleniumはFirefoxのバージョンによって動くバージョンが異なります。
適宜環境に合わせて変更してください。
`wget http://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar`

### CentOSの日本語化
日本語化しないと、日本語が文字化けします。
今回はCentOS7の日本語化です。

```
[root@localhost behat]# yum -y install ibus-kkc vlgothic-*
[root@localhost behat]# localectl set-locale LANG=ja_JP.UTF-8
[root@localhost behat]# source /etc/locale.conf
```

## behat準備
### composer準備
composerでbehatをインストールします。
任意のディレクトリで
`composer init`
を実行し、composer.jsonに下記を記載します。
(composer本体のインストール方法は様々な記事があるので省略します。)

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

記載後、`composer install`を実行してください。

### behatの初期化
composer installが完了したあと、実行したディレクトリにbinディレクトリが生成されます。
`./bin/behat --init`を実行し、behatの初期化を行ってください。

```
[root@localhost behat]# ./bin/behat --init
+d features - place your *.feature files here
+d features/bootstrap - place your context classes here
+f features/bootstrap/FeatureContext.php - place your definitions, transformatio
ns and hooks here
```

### behatの設定
behat.ymlをサンプルからコピーして設定を追加します。
`cp ./vendor/behat/mink-extension/behat.yml.dist behat.yml`

```behat.yml
default:
  suites:
    default:
      path: %paths.base%/features
      # FeatureContextを追加
      contexts: [FeatureContext,Behat\MinkExtension\Context\MinkContext]
  extensions:
    Behat\MinkExtension:
      # googleで検索シナリオにしたいので、base_urlを変更
      base_url: https://www.google.co.jp/
      # firefoxで検索する
      browser_name: firefox
      sessions:
        default:
          selenium2:
            capabilities: {"browser":"firefox","version":"*"}
```

## シナリオ記述方法
### 登録シナリオの確認
現在登録されている有効なシナリオは下記コマンドで確認できます。

```
[root@localhost behat]# ./bin/behat -dl
default | 前提< /^(?:|ユーザーは )ホームページを表示している$/
default | もし< /^(?:|ユーザーは )ホームページへ移動する$/
default | 前提< /^(?:|ユーザーは )"(?P<page>[^\s]+)" を表示している$/u
default | もし< /^(?:|ユーザーが )"(?P<page>[^\s]+)" へ移動する$/u
default | もし< /^(?:|ユーザーが )ページをリロードする$/u
default | もし< /^(?:|ユーザーが )履歴の前のページに戻る$/u
default | もし< /^(?:|ユーザーが )履歴の次のページヘ進む$/u
default | もし< /^(?:|ユーザーが )"(?P<button>(?:[^"]|\\")*)" ボタンをクリックす
る$/u
default | もし< /^(?:|ユーザーが )"(?P<link>(?:[^"]|\\")*)" のリンク先へ移動する
$/u
default | もし< /^(?:|ユーザーが )"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<v
alue>(?:[^"]|\\")*)" と入力する$/u
default | もし< /^(?:|ユーザーが )"(?P<field>(?:[^"]|\\")*)" フィールドに以下の
値を入力する:$/u
default | もし< /^(?:|ユーザーが )"(?P<value>(?:[^"]|\\")*)" という値を "(?P<fie
ld>(?:[^"]|\\")*)" に入力する$/u
default | もし< /^(?:|ユーザーが)次のように入力する:$/u
default | もし< /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" という値を "(?P<se
lect>(?:[^"]|\\")*)" から選択する$/u
default | もし< /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" という値を "(?P<se
lect>(?:[^"]|\\")*)" から追加で選択する$/u
default | もし< /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" にチェックをつける
$/u
default | もし< /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" のチェックをはずす
$/u
default | もし< /^(?:|ユーザーが)パス "(?P<path>[^"]*)" にあるファイルを "(?P<fi
eld>(?:[^"]|\\")*)" に添付する$/u
default | ならば< /^(?:|ユーザーが )(?P<page>[^\s]+) を表示していること$/u
default | ならば< /^(?:|ユーザーが )ホームページを表示していること$/u
default | ならば< /^(?i)url(?-i)が (?P<pattern>"(?:[^"]|\\")*") にマッチすること
$/u
default | ならば< /^レスポンスコードが (?P<code>\d+) であること$/u
default | ならば< /^レスポンスコードが (?P<code>\d+) ではないこと$/u
default | ならば< /^(?:|画面に )"(?P<text>(?:[^"]|\\")*)" と表示されていること$/
u
default | ならば< /^(?:|画面に )"(?P<text>(?:[^"]|\\")*)" と表示されていないこと
$/u
default | ならば< /^(?:|画面に )"(?P<pattern>"(?:[^"]|\\")*")" にマッチするテキ
ストが表示されていること$/u
default | ならば< /^(?:|画面に )"(?P<pattern>"(?:[^"]|\\")*")" にマッチするテキ
ストが表示されていないこと$/u
default | ならば< /^レスポンスに "(?P<text>(?:[^"]|\\")*)" が含まれていること$/u

default | ならば< /^レスポンスに "(?P<text>(?:[^"]|\\")*)" が含まれていないこと$
/u
default | ならば< /^"(?P<element>[^"]*)" エレメントに "(?P<text>(?:[^"]|\\")*)"
と表示されていること$/u
default | ならば< /^"(?P<element>[^"]*)" エレメントに "(?P<text>(?:[^"]|\\")*)"
と表示されていないこと$/u
default | ならば< /^"(?P<element>[^"]*)" エレメントに "(?P<value>(?:[^"]|\\")*)"
 という値が含まれていること$/u
default | ならば< /^"(?P<element>[^"]*)" エレメントに "(?P<value>(?:[^"]|\\")*)"
 という値が含まれていないこと$/u
default | ならば< /^(?:|画面に )"(?P<element>[^"]*)" エレメントが表示されている
こと$/u
default | ならば< /^(?:|画面に )"(?P<element>[^"]*)" エレメントが表示されていな
いこと$/u
default | ならば< /^"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<value>(?:[^"]|\
\")*)" が含まれていること$/u
default | ならば< /^"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<value>(?:[^"]|\
\")*)" が含まれていないこと$/u
default | ならば< /^(?:|画面に )(?P<num>\d+) 個の "(?P<element>[^"]*)" エレメン
トが表示されていること$/u
default | ならば< /^チェックボックス "(?P<checkbox>(?:[^"]|\\")*)" のチェックが
ついていること$/u
default | ならば< /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" (?:is|should be)
checked$/
default | ならば< /^チェックボックス "(?P<checkbox>(?:[^"]|\\")*)" のチェックが
はずれていること$/u
default | ならば< /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" should (?:be unch
ecked|not be checked)$/
default | ならば< /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" is (?:unchecked|n
ot checked)$/
default | ならば< /^現在のURLを表示$/
default | ならば< /^最後のレスポンスを表示$/u
default | ならば< /^最後のレスポンスをブラウザで表示$/u
```

### シナリオ記述方法（1回テスト）
1回のみテストを行うシナリオを記載します。
今回はgoogleでQiitaを検索するシナリオを記載します。

``` features/test1.feature
# language: ja
#
フィーチャ: グーグルでQiitaを検索

    @javascript
    シナリオ: グーグルでQiitaを検索
        前提 ホームページを表示している
        もし "q" フィールドに "Qiita" と入力する
            かつ "Google 検索" ボタンをクリックする
        ならば "https://qiita.com/" と表示されていること
```

### シナリオ記述方法（繰り返しテスト）
繰り返しテストを行うシナリオを記載します。
今回はgoogleでQiitaとQiita Advent Calendarを検索するシナリオを記載します。

``` features/test2.feature
# language: ja
#
フィーチャ: グーグルでQiitaとQiita Advent Calendarを検索

	@javascript
	シナリオテンプレート: グーグルでQiitaとQiita Advent Calendarを検索
		前提 ホームページを表示している
		もし "q" フィールドに "<search>" と入力する
		かつ "Google 検索" ボタンをクリックする
		ならば "<URL>" と表示されていること

	例:
		| search | URL |
		| Qiita | https://qiita.com/ |
		| Qiita Advent Calendar | https://qiita.com/advent-calendar |
```

「シナリオ：」となっている箇所が「シナリオテンプレート：」になっています。
また、繰り返しテストの中で、値を変えたい箇所を<>で囲むことで変数として入力されます。
変数は「例：」として記述します。

## シナリオ実行
CentOS上で完結させるため、ブラウザを起動させます。


* Xvfb(仮想ディスプレイ)をバックグラウンドで起動
`Xvfb :1 -screen 0 1024x768x24 &`

* firefoxをバックグラウンドで起動
` firefox &`

* 起動の確認
`jobs`

また、Seleniumも起動させます。
`java -jar selenium-server-standalone-2.53.1.jar`

Seleniumの起動が完了を待ちましょう。
(下記ログを確認)
`INFO - Selenium Server is up and running`

すべての起動が完了したらシナリオを実行させます。
まずは、test1の実行をします。

```test1.log
[root@localhost behat]# ./bin/behat features/test1.feature
フィーチャ: グーグルでQiitaを検索

  @javascript
  シナリオ: グーグルでQiitaを検索                       # features/test1.feature:6
    前提 ホームページを表示している                    # Behat\MinkExtension\Context\MinkContext::iAmOnHomepage()
    もし "q" フィールドに "Qiita" と入力する         # Behat\MinkExtension\Context\MinkContext::fillField()
    かつ "Google 検索" ボタンをクリックする           # Behat\MinkExtension\Context\MinkContext::pressButton()
    ならば "https://qiita.com/" と表示されていること # Behat\MinkExtension\Context\MinkContext::assertPageContainsText()

1 個のシナリオ (1 個成功)
4 個のステップ (4 個成功)
0m28.30s (9.60Mb)
```

すべて成功を確認しました。
続けて、test2を実行します。

```test2.log
[root@localhost behat]# ./bin/behat features/test2.feature
フィーチャ: グーグルでQiitaとQiita Advent Calendarを検索

  @javascript
  シナリオテンプレート: グーグルでQiitaとQiita Advent Calendarを検索 # features/test2.feature:6
    前提 ホームページを表示している                    # Behat\MinkExtension\Context\MinkContext::iAmOnHomepage()
    もし "q" フィールドに "<search>" と入力する         # Behat\MinkExtension\Context\MinkContext::fillField()
    かつ "Google 検索" ボタンをクリックする           # Behat\MinkExtension\Context\MinkContext::pressButton()
    ならば "<URL>" と表示されていること # Behat\MinkExtension\Context\MinkContext::assertPageContainsText()

    例:
      | search                | URL                               |
      | Qiita                 | https://qiita.com/                |
      | Qiita Advent Calendar | https://qiita.com/advent-calendar |

2 個のシナリオ (2 個成功)
8 個のステップ (8 個成功)
0m18.39s (10.96Mb)
```

すべて成功を確認しました。

## はまったこと
* GLIBCXXが全然足りないって言われる
    * CentOS6系だとでます。最新のgccをビルドすると直るようですが、かなり時間がかかります。CentOS7を使用することをおすすめします。
* Firefoxが起動しない
    * ディスプレイの設定が間違えている or 設定が欠けていると起動しないようです。

## まとめ
最初にシナリオを作成するのは結構面倒ですが、一度作ってしまえば繰り返しテストはかなり楽になるかと思います。
今の状態ではテスト結果がログでしかわからないため、次は画面キャプチャを残す方法を投稿します。

## 参考
[Behatで行う、E2Eテスト入門](https://www.slideshare.net/leverages_event/ss-42559237)

[Behat+Selenium Webdriverで受け入れテストの自動化をやってみた](https://7me.oji.0j0.jp/2016/behat-selenium-webdriver.html)

[Vagrant の CentOS で Selenium テストを回したい - Qiita](http://qiita.com/shixn/items/49a926fdbd298a87647d)