関数やクラスがないプロジェクトでも仕様を把握する為にテストを書く

<a href="http://qiita.com/advent-calendar/2016/softwaretesting" target="_blank">ソフトウェアテスト Advent Calendar 2016</a>の25日目の記事です。

ユニットテストをCIでまわす。

エラーの発見に限らず仕様の確認やプログラマの不安要素を取り除く素晴らしい仕組みですが、残念ながらそれを簡単に適用できるプロジェクトばかりではありません。

実際、私が今年参加したWebサイトのプロジェクトはテスト対象となるユニット（クラス・関数）が存在せず、Pureなphpが数百と複雑に絡み合って構築されていました。
もちろん、実装者は退職済みでドキュメント類は少ないかメンテナンスされておらず古いままです。

今回はこのようなケースにSelenium WebDriverを用いた仕様化テストについて書きたいと思います。


## 仕様化テストについて

レガシーコード改善ガイドで紹介されている手法で、既存コードが実際にどのように動くかをテストによって確認していきます。
テストファーストの場合は「テストでどのように動くべきか」を記述し、処理を実装していくので逆から辿るような感じになります。

### 仕様化テストをページに適応する

レガシーコード改善ガイドで紹介されている手順は以下の通りです。

> 1. テストハーネスの中で対象のコードを呼び出す
> 2. 失敗することが分かっている表明を書く
> 3. 失敗した結果から実際の振る舞いを確認する
> 4. コードが実現する振る舞いをするように、テストを変更する
> 5. 以上の手順を繰り返す

1に関してはページを呼び出してコードを実行し、ページの仕様を確認していきます。

2,3に関しては失敗するというよりはページのドキュメントがなく、挙動がどうなるかわからないケースを対象にします。


## 環境構築

プロダクトの環境はスタンダードなLAMP環境です。

* CentOS 7.2
* apache 2.4
* php 5.6
* mysql 5.6

開発環境には別途、以下を追加します。

* openjdk 1.8.0
* firefox 45.6.0
* selenium standalone server 3.0.1
* geckodriver 0.11.1
* Xvfb

ヘッドレスブラウザを使うという選択肢もありますが、可能な限りユーザ環境と合わせた方が良いのは自明なので仮想ディスプレイでfirefoxを動かします。

```bash
# yumから入れられる必要なコマンド類をインストール
sudo yum install java-1.8.0-openjdk.x86_64 java-1.8.0-openjdk-devel.x86_64 xorg-x11-server-Xvfb firefox
sudo yum groupinstall "Japanese Support"

## selenium standalone server、GeckoDriverをダウンロード
curl -LO http://selenium-release.storage.googleapis.com/3.0/selenium-server-standalone-3.0.1.jar
curl -LO https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz
tar -xzpvf geckodriver-v0.11.1-linux64.tar.gz
```

### phpでのWebDriver環境構築

ブラウザを操作する為のSelenium WebDriverはサードパーティ製を含めれば多様な言語に用意されています。

ブラウザを使った自動化テストのシェア的にはrubyを選択しているプロジェクトが多いように感じます。
テストの書きやすさや日本語の本が出ていることも大きいかと思いますが、オフィシャルからWebDriverが出ているのもアドバンテージですね。

今回はプロダクトの言語と合わせる為にphpを選択しました。
残念ながらphpはオフィシャルからは出ていませんが<a href="https://github.com/facebook/php-webdriver" target="_blank">facebook製のWebDriver</a>があるので安心感はあります。
ありがたく使わせていただきます。

環境構築例です。

```bash
composer.phar require-dev facebook/webdriver
composer.phar require-dev phpunit/phpunit
```

テストランナーとして使用するphpunitもインストールしています。

バージョンは指定しませんでしたが下のバージョンがインストールされました。

* facebook/webdriver 1.2
* phpunit/phpunit 5.7.4


## 仕様化テストの実装

仕様が不明で把握したい箇所のテストを記述しています。

今回は使用していませんが、ページの操作をページオブジェクトとして切り出すPage Object Patternを使用して実装すると、ページ操作とテストコードの分離ができて見通しが良くなります。

http://www.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern

今回はトップページにメインキービジュアルのスライドが使用されていたのですが、
スライド画像が0～複数と異なるケースでどのように動作が変わるのかをコードで確認することを例とします。

想定したケースは以下の通りです。

* 0枚の場合はスライド、コントロール（next/prev、pagination）が表示されない
* 1枚の場合は画像が1枚表示され、コントロールは表示されない
* 2枚以上の場合はスライドもコントロールも表示される

複数ケースの想定が考えられますが基本的な考え方として、複数のことを一度にやってはいけないというのを厳守します。
焦らずに１つ１つ順に仕様化テストの手順をまわしていきます。

0枚の時の挙動を確認するコード例です。

```php

class TopPageTest extends TestCase
{
    private static $driver;

    public static function setUpBeforeClass()
    {
        $selenium_host = 'http://127.0.0.1:4444/wd/hub';
        $desired_capabilities = DesiredCapabilities::firefox();

        self::$driver = RemoteWebDriver::create($selenium_host, $desired_capabilities, 5000);
        self::$driver->manage()->window()->maximize();
    }

    // （中略）

    /**
     * @test
     */
    public function メイン画像が0枚の時メインキービジュアルは表示されない()
    {
        $driver = $this->getDriver();
        $mainImage = $this->getMainImageModel();

        // メイン画像を0にする
        $mainImage->deleteAll();

        $driver->get('https://（テストサイト）/');

        // 表示対象のスライドがない場合はスライド要素が非表示になるはず
        $this->assertEquals(0, count($driver->findElements(WebDriverBy::cssSelector('.スライドを囲む要素に適用したクラス名'))));
    }

```

特定のクラスを持つ要素が存在するかどうかで仮説が正しいか確認しています。

テストを実行するにはテストを実行するより先にselenium standalone serverの起動と仮想ディスプレイXvfbの起動が必要です。

```bash
export DISPLAY=:99
java -Dwebdriver.firefox.bin="/usr/bin/firefox" -Dwebdriver.gecko.driver="/ダウンロードしたパス/geckodriver-v0.11.1" -jar selenium-server-standalone-3.0.1.jar
```

環境変数DISPLAYを設定するのはseleniumのサーバを起動する前にしないとWebDriverがselenium serverに通信できずに落ちるので気を付けてください。

geckodriverについてはseleniumとfirefoxのバージョンの組み合わせによって要・不要が変わってきます。
その辺りは、先日参加した勉強会で発表された下記スライドがとても分かりやすくまとまっているので一読するのをお勧めします。
[2016 Seleniumゆく年くる年 @ 第4回 日本Seleniumユーザーコミュニティ勉強会](http://www.slideshare.net/hiroshitoda39/2016-selenium-4-selenium-70244281)

仮想ディスプレイであるXvfbには特定のスクリプトを実行する度に起動と終了をしてくれるxvfb-runという便利なコマンドがあるのでこれを使用します。

```bash
xvfb-run --server-num=99 --server-args="-screen 0 1024x768x24" ./vendor/bin/phpunit
```

実行してみたところ、見事にエラーが発生しました。
phpでエラー時に発生するページが真っ白になる状態です。

ログを確認してみると画像が0のケースは考慮されておらずforeachにbooleanを渡して落ちていました。
このことを上に確認してみると「画像が0になるケースなんてまずないから問題ない」とのこと。

ということでメイン画像が0件だとトップページはエラーで落ちるという仕様が明確化しました:angel:

このような流れを繰り返せばサイトの仕様を把握できるし、コードとして確認できるようになります。

## 最後に

仕様化テストをWebページに適用する例を紹介しました。

どのようなプロジェクトに参加するにしても既存プロダクトを把握することは最初にやらなくてはならない重要な手順です。
関数やクラスが無いからテストが書けないと悩んでいる方がいたら参考にしてみてください。

ただ普通のクラスに対するユニットテストと比較すると工数はかかるし、ブラウザの自動操作になるので多少flakyになりやすいので全部を全部この手法でコード化するのは馬鹿げています。
適用部分はクリティカルな部分やどうしても気になる部分に抑えておいた方がよいでしょう。

もちろん、このようなプロジェクトを避けるという選択肢があるならそれも一つの選択肢だと思いますが...