PHPでスモークテストのサンプルを書いてみて感じたこととか

ソフトウェアテストの勉強中&実際の案件に活用中。

スモークテストとは

ソースコードに修正が入るなどして新しくビルドが必要になった際に、
そのビルドが正常に終了していることを確認するためのテストです。

簡単に言えば、「ビルドによってどこか動かなくなったりしてないよね?」を確認するテスト。
要件テスト(画面テストとか)を始める前の、そもそもテスト出来る状態にあるかどうかをテストするテスト。
テストのためのテスト。テスト。

ソースコードの開発・追加・修正を終えたソフトウェアが動作する状態にあるかを確認するテストのこと。本格的なソフトウェアテストが実施可能かを確認するための予備的な簡易テストである。
情報システム用語事典:スモークテスト(すもーくてすと) - ITmedia エンタープライズ

言葉の由来はハードウェアのテストにあるらしい。
新しい基盤に対して電源を入れて、モクモクと煙が出たらアウトってのを試すものだとか。

PHPにおけるスモークテストとは

PHPにはビルドはありません。
ので、ビルドという用語をそのまま当てはめるのではなくちょっと考えてみた。

スモークテストの説明ではほとんどが「ビルドしたソフトが〜」みたいな書き方だけど、
要は「修正したモジュールがちゃんと動くの?」ってことをあらかじめ確認しておきたいってことなわけで。
(「ちゃんと」とは、仕様通りという意味ではなくエラーなく動くことを指す)

となると、PHPでは大前提「Fatal errorが出るようになったりしてないよね?」を確認すればいいと思いました。
(Warningも検知したいけど今回のコードにはない)
そしてそう考えると、「これは思ったより不安解消の効果があるかもしれない」と思いました。
PHPではJavaなどのようにある程度のミスをコンパイラが検知してくれるなんてことがないので、
ちょっとしたことで意図せぬところがデグっていたりバグっていたりというのはよくあります。

でもそれを確認するために画面テストを書くとなると工数がかかるため、なかなかに億劫だったりします。
手動で毎回確認も非効率極まりないですね。
なので「とりあえずエラーは起きてないぞ」をサクッと確認出来るというのは、心的に結構大きい。

何より大して時間かけないで書けるものなら、何もないより遥かにいいよね。

サンプルコード(with PHPUnit)

こんな感じのコードを書いてみました。PHPUnit使ってます。

<?php
class SmokeTest extends PHPUnit_Framework_TestCase {
    public static function setUpBeforeClass() {
        if(!is_dir(self::captureDirectory())) {
            mkdir(self::captureDirectory());
        }
    }

    private static function captureDirectory() {
        return dirname(__FILE__) . '/capture';
    }

    /**
     * @test
     * @dataProvider pagePathProvider
     */
    public function FatalErrorが発生していないこと($pagePath) {
        $url = "http://hoge.com/{$pagePath}";
        $response = $this->request($url);
        // とりあえず簡単な文字列抽出(エラーが表示されること前提)
        $actual = preg_match('/Fatal error.*/', $response, $matches);
        if(($actual === false) || ($actual !== 0)) {
            // Errorがあったのならレスポンス全体をログとしてファイルに保存
            $filename = date('Y-m-d_') . urlencode($url) . '.response.txt';
            file_put_contents(self::captureDirectory()."/{$filename}", $response);
            $this->assertTrue(false, $matches[0]); // テスト失敗させる
        } else {
            $this->assertTrue(true);
        }
    }

    public function pagePathProvider() {
        return array(
            array(""),
            array("page1"),
            array("page2"),
        );
    }

    private function request($url) {
        $ch = curl_init();
        // リバースプロキシなどでキャッシュが効くと困るので
        $headers = array('Pragma: no-cache');
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $data = curl_exec($ch);
        curl_close($ch);
        return $data;
    }
}

エラーが画面に直接表示されることを前提に書きました。
本番運用ではもちろん有り得ませんが、開発環境では出力されるのでひとまず。

失敗した場合の出力はこんな感じ。

$ phpunit SmokeTest.php
F

Time: 5 seconds, Memory: 3.75Mb

There was 1 failure:

1) SmokeTest::FatalErrorが発生していないこと with data set #0 ('http://hoge.com/')
Fatal error:  Call to a member function aa() on a non-object in /home/user/hogehoge.php on line 76
Failed asserting that is true. C:\workspace\test\smoke\SmokeTest.php:24 FAILURES! Tests: 1, Assertions: 1, Failures: 1.

Fatal errorの文言が失敗メッセージとして出力されます。
また、この時のHTTPレスポンス全体はログとしてファイルに保存されます。

画面テストをcurlベースでやるかSeleniumでやるか

これを書いてみて思いました。
「簡単な画面の表示確認ならこれをベースにテキスト解析でいいんじゃないか」

今まで画面テスト=Selenium使うっていう、ちょっと固定観念みたいなものがありました。
でもSeleniumの実行ってSeleniumサーバー立ち上げなきゃいけないし、実行中はブラウザが開いては閉じてで邪魔だし、重いし。
と、ちょっと億劫な面が個人的にありました。
テストサーバー/CIサーバーで実行すればいいじゃんって話もありますが、そんな環境ないし。

で、先日も書いたけどUI要素を指定してのテストケースを書くっていうのもダルい。
もちろんSeleniumにだって単純なテキストassertがあるのでそれを使うのもよいでしょうが、
だったらHTTPレスポンス受け取ってテキスト解析するのと変わらないよね、と。

Seleniumの全てが不要というわけじゃなくて、使い分けをした方がいいんじゃないかと気付いたんです。
今思いつくところだと例えばこんな感じ。

HTTPレスポンスを自前で処理

  • 向き
    • テストコードは基本のPHP関数だけで書けるはずなので学習コスト低い
    • (Seleniumはその書き方をメンバーに覚えてもらう必要がある)
    • ブラウザ立ち上げとかしないのでSeleniumよりは早いかも
    • 普通のユニットテストのように扱えるのでautotestとかStagehandによるテスト自動実行しっぱなしが楽
    • UA変えるのはheaderに追加すればいいから簡単
    • タイムアウトのチェックとかもSeleniumと同じく簡単に書ける(curlの引数)
    • Selenium使わないことが今の環境的にはビルドプロセスに組み込みやすい
  • 不向き
    • (あえて)UI要素を厳密にチェックしたい場合はめんどすぎる。気がする
    • セッション扱うなど状態遷移のテストケース
    • ユーザー操作が必要なテストケース

Seleniumは反対に、自前処理が得意とするところが「同じぐらい」か「ちょっと苦手、面倒」で、
自前処理が苦手とするところを「楽に書ける」ように思いました。
どうなんだろ。
個人的には自前処理は「導入が楽」ってのと「Selenium覚えてもらう必要がない」ってのが大きい。


テストの目的はなにで、その自動化をなぜその手法でやる必要があるのか。
それを自分の環境を踏まえて考えてみると、上手いこと切り分けられてハッピーになれるかも。
上記コードはまだまだ雛形なので、これをベースに使いやすく拡張していきます。