Behatでテスト実行時に自動的にスクリーンショットを撮る

# はじめに
先日 @kurudrive さんがWordPressの公開テーマ、BillVektor用にBehatを使う記事が上げられていました。

[WordPressの要件テストを自動化＆スクリーンショットで保存出来るBDDテストがすごい！](https://www.vektor-inc.co.jp/wordpress-tech/bbd-test/)

丁寧にまとめられている上に公開されているプロジェクトなのですごく参考になりました。
僕も先日の案件で初めてBehatを使ってみて、そちらで取り入れた「テスト時に自動でスクリーンショットを撮る機能」便利だったのでそちらについてまとめてみます。

# Behatを使った案件の概要
WordPressを使った管理システムの開発プロジェクトでした。
アサインされているプログラム開発メンバーは自分を含めて3人。
二ヶ月ほどの開発期間のあるプロジェクトでした。
kurudriveさんと同じく、ちょうど僕も宮内さんのPHPカンファレンスであったBehatの発表を聞いていたので、実際の案件で近々取り入れたいと思っていました。
Behat自体はWP-CLIで何度か読んでいて、「なるほどこれ流行りのがBDDか」と思っていました。

[Behatを使ってWordPressの構成をテストする](https://speakerdeck.com/miya0001/behatwoshi-tutewordpressfalsegou-cheng-wotesutosuru)

# ざっくりとした説明
BehatはBehavior Driven Development(振る舞い 駆動 開発)です。
テスト駆動開発の場合には多くがユニットテストで構成されています。
クラス化されたロジック周りはユニットテストが適していますが、それに対してBDDではクラス間をまたがった結合テストを行うことができます。
特にユーザの操作をベースにした振る舞いを記述する場合、E2Eテストと呼ばれているようです。

# Dockerでの実行
今回の環境はDockerを利用しました。
ベースについてはこちらのDocker開発環境をベースにしました。

[ローカル開発環境をもっとたくさんの人に使ってもらいたくてDockerで作りました](http://qiita.com/yousan/items/f05fa03c1f3951971f2f)

これのdocker-compose.yamlにSeleniumを追加しています。

# Behatのコード
今回のテストコードはこちらです。
https://github.com/yousan/behat-example

## docker-compose
Selenium用のdocker-compose.yamlファイルです。

```yaml
version: '2'

services:

  php5.6-apache.dev:
    image: yousan/php5.6-apache
    ports:
      - 80:80
    volumes:
      # Add symlink directories if you use symlinks at documents.
      - ~/git:/Users/yousan/git
      - ~/public_html:/var/www/vhosts
    environment:
      PMA_HOST: mysql.dev
      PMA_USER: root
      PMA_PASSWORD: example

  mysql.dev:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: example
    ports:
      - 3306:3306
    volumes:
      - /private/var/lib/mysql:/var/lib/mysql

  dnsmasq.dev:
    image: yousan/dnsmasq
    ports:
      - 53:53/tcp
      - 53:53/udp
    cap_add:
      - NET_ADMIN

  selenium.dev:
    image: selenium/standalone-chrome-debug:latest
    ports:
      - 4444:4444/tcp
      - 4445:5900/tcp
    external_links:
      - php5.6-apache.dev:behat-example.dev
```

このdocker-composeでは
1. Apache + mod_php
2. MySQL (MariaDB)
3. DNSサーバ
4. Seleniumホスト
が動きます。

このSeleniumのコンテナは内部でLinuxのX Window Systemが立ち上がり、その中でChromeを動かします。
BehatはこのSeleniumコンテナ上で動きます。
イメージのstandalone-chrome-debugはデバッグ用で、Xに対してVNCで接続することができます。


MacであればFinderの「サーバに接続（コマンド+k）」でVNC接続ができます。

<img width="482" alt="Screen Shot 2017-08-15 at 16.32.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/47327/224696e7-b74a-fbf6-bee0-f5ef7962ca4e.png">

<img width="480" alt="Screen Shot 2017-08-16 at 15.16.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/47327/b9370070-12b0-09db-d65a-560a7a57eccb.png">


このdocker-composeファイルでは *.dev のDNS解決も行っています。
注意点として、Macの場合には *.dev をlocalhostで解決できるようにしておきます。

http://qiita.com/ono_matope/items/cd3be40b5179731d4460

## Behat
一番メインの箇所ですね。
BehatのbootstrapにFeatureContextを記述して、各種テストが通った時にスクリーンショットを撮るようにします。

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

use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\AfterStepScope;
use Behat\Gherkin\Node\StepNode;

/**
 * Defines application features from the specific context.
 */
#class FeatureContext extends Behatch\Context\BaseContext
class FeatureContext extends \Behat\MinkExtension\Context\RawMinkContext implements Context {
	private $screenshotDir;

	/**
	 * Initializes context.
	 *
	 * Every scenario gets its own context instance.
	 * You can also pass arbitrary arguments to the
	 * context constructor through behat.yml.
	 *
	 * @param string $screenshotDir
	 */
	public function __construct( $screenshotDir = '.' ) {
		$this->screenshotDir = $screenshotDir;
	}

	/**
	 * Copy from vendor/behatch/contexts/src/Context/DebugContext.php:46
	 * テスト実行時にスクリーンショットを撮影する
	 * behatchのDebugContextはテストが失敗した時、かつ@javascriptタグがフィーチャーに付いている時のみ動作する。
	 *   (Driverの実装によってはsession->driver->getScreenshot()が動作しない)
	 * ファイル名を決定する際にシナリオ名をurl_encodeしているため、ファイル名が長くてfile_put_contentsに失敗する。
	 *
	 *
	 * @AfterStep
	 *
	 * @param AfterStepScope $scope
	 */
	public function saveScreenshotsAfterStep(AfterStepScope $scope)
	{
		#if (! $scope->getTestResult()->isPassed()) {
		#$makeScreenshot = false;
		$suiteName      = str_replace(' ', '_', $scope->getSuite()->getName());
		$featureName    = str_replace(' ', '_', $scope->getFeature()->getTitle());
		if ($this->getBackground($scope)) {
			#$makeScreenshot = $scope->getFeature()->hasTag('javascript');
			$scenarioName   = 'background';
			$stepLine       = 0;
		} else {
			$scenario       = $this->getScenario($scope);
			#$makeScreenshot = $scope->getFeature()->hasTag('javascript') || $scenario->hasTag('javascript');
			$scenarioName   = str_replace(' ', '_', $scenario->getTitle());
			$stepLine       = $scope->getStep()->getLine();
		}
		#if ($makeScreenshot) {
		$isPassed = $scope->getTestResult()->isPassed() ? "pass" : "fail";
		$filename = sprintf( '%s_%s_%s_%s_%s_%s.png', $isPassed, round( microtime( true ) * 1000 ), $suiteName, $featureName, $scenarioName, $stepLine );
		$this->saveScreenshot( $filename, $this->screenshotDir );
		#}
		#}
	}

	/**
	 * StepからScenarioを直接辿れない構造になっているため
	 * StepからFeatureを取得してScenario一覧を取得して
	 * 一覧をforeachで回して実行中のStepの行番号に該当するScenarioを取得するメソッドっぽい。カオス。
	 * @param AfterStepScope $scope
	 * @return \Behat\Gherkin\Node\ScenarioInterface
	 */
	private function getScenario(AfterStepScope $scope)
	{
		$scenarios = $scope->getFeature()->getScenarios();
		foreach ($scenarios as $scenario) {
			$stepLinesInScenario = array_map(
				function (StepNode $step) {
					return $step->getLine();
				},
				$scenario->getSteps()
			);
			if (in_array($scope->getStep()->getLine(), $stepLinesInScenario)) {
				return $scenario;
			}
		}

		throw new \LogicException('Unable to find the scenario');
	}

	/**
	 * @param AfterStepScope $scope
	 * @return \Behat\Gherkin\Node\BackgroundNode
	 */
	private function getBackground(AfterStepScope $scope)
	{
		$background = $scope->getFeature()->getBackground();
		if(!$background){
			return false;
		}
		$stepLinesInBackground = array_map(
			function (StepNode $step) {
				return $step->getLine();
			},
			$background->getSteps()
		);
		if (in_array($scope->getStep()->getLine(), $stepLinesInBackground)) {
			return $background;
		}

		return false;
	}
}
```

上記のコード中で定義していますが、スクリーンショットのファイル名は
「テスト結果 _ 時間 _ 機能名 _ シナリオ名」
となります。

こんな感じです。

<img width="737" alt="Screen Shot 2017-08-18 at 11.57.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/47327/7ebda889-1f16-24db-963f-ad0e663083a7.png">


スクリーンショットを撮るためにbehat.ymlに下記の設定を入れておきます。

```yaml:behat.yml
     - FeatureContext:
        screenshotDir: %paths.base%/screenshot
```

コードはこちらにあります。

https://github.com/yousan/behat-example/blob/master/tests/behat.yml


## Composer

テストはComposerで走らせています。　

```json:composer.json
  "scripts": {
    "test": [
      "composer test-behat"
    ],
    "test-behat": [
      "./vendor/bin/behat --config ./tests/behat.yml"
    ],
```

ローカルにBehatをインストールしても良いですが、ここではComposerのバイナリを使っています。

全体のコードは下記にあります。
https://github.com/yousan/behat-example/blob/master/composer.json

Composerで動かすとこんな感じです。

```bash
$ composer behat-test 
```

<img width="552" alt="Screen Shot 2017-08-18 at 12.04.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/47327/0d6675ce-b6eb-58d6-1a6b-8af5558f5dcd.png">


# 感想
以上でBehatのテスト時に自動的にスクリーンショットを撮ることができます。
スクリーンショットを撮ることで、テストのパス、フェイルの判定もそうなのですが、FinderやExplorerなどのファイラーで簡単に結果を目視確認できることはすごく便利だと思いました。
特にWeb開発の実務よりだと、HTMLタグに不整合が発生してカラム落ちやズレが起きることが多く、それらをパパパッと確認できます。
その辺りのメリットついては別途まとめたいと思います。

また @miya0001 さんのボイラープレートではPhantom.JSで実装していたので、Seleniumではなくその方法でもやってみたいです。
[WordPressに対してBDDなテストを行うためのボイラープレートをつくった](https://firegoby.jp/archives/6871)

また今回のコード、特に核心のBehat周りについては @naname さんが書きました。というかBehatについてほぼ全てを教えてもらいました。ありがとうございます！

# 参考サイト
https://www.vektor-inc.co.jp/wordpress-tech/bbd-test/
http://www.atmarkit.co.jp/ait/articles/1403/05/news035_3.html
http://qiita.com/seiya1121/items/95dad2bbb489d3fa47ff