ぷぎがぽぎ このページをアンテナに追加 RSSフィード

2013-03-17

[PHP] Hamcrestを利用した超シンプルdocコメントでのテスト方法

PHPでテストを書くというとPHPUnitデファクトスタンダードで、次がSimpleTestでしょうか。

以前はインストールも大変でしたが、今となってはcomposer使えば楽ですし、実績もあります。

でも、本当にこの2択でPHPらしい開発ができていますか?

たとえば、テストケースのクラスを用意することが前提になります。

ちょっとPHPのコードを書いてテストしたいときもです。

たとえば、以下のようなロジックを書きたいとします。

<?php

$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];

// この$usersから男性('male')のものだけを抽出したい

$males = [
    '太郎' => 'male',
    '一郎' => 'male',    
];

?>

普通にPHPUnitでTDDでとなると、それなりに面倒です。

まず、ソースファイル(filter.php)とテストクラスファイル(filterTest.php)を作らなくてはなりません。

とりあえずさくっとコードを書きながらテストもしたいときは準備が面倒です。

最初にテストを書くとすると次のような感じでしょうか。

  • filterTest.php
<?php
require_once __DIR__ . '/filter.php';

class FilterTest extends PHPUnit_Framework_TestCase
{
    public function filterMales()
    {
        $users =[
	    '太郎' => 'male',
	    '花子' => 'female',
	    '一郎' => 'male',
        ];

        $expected = [
            '太郎' => 'male',
	    '一郎' => 'male',
        ];

        $this->assertEquals($expected, filterMale($users));
    }
}

やりたいコードの書き方

ソースとテストコードをいったりきたりせずに、コメントのように埋め込める方法があるといいですよね

以下のような感じで、ソースとテストを織り交ぜながらコードを書いていくパターンです。

<?php
$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];

// この$usersから男性(male)のものだけを抽出したい

$males = [
    '太郎' => 'male',
    '一郎' => 'male',    
];

function filterMale($users)
{
 .....
}

/**
 * @assertThat(filterMale($users), is($males)); <= このコメントがここでテストとして実行される。
 */

// 女性だけ抽出したい

$females = [
    '花子' => 'female',
];

function filterFemale($users)
{
 .....
}

/**
 * @assertThat(filterFemale($users), is($females)); <= このコメントがここでテストとして実行される。
 */

どうですか?直感的でシンプルでいい感じじゃないですか?

もちろんこのままプロダクトコードとして使うのはアレですが、PHPでのちょっとしたコードを試行錯誤しながら書いているときにこのように書けると良いですよね。


ルールは以下のような感じ

  • docコメントで@assertThatで定義したテストケースが実行される
    • assertThatが何というのはのちほどちらっと説明
  • 別テスト用のファイルが不要
  • コメントなので、実装には影響なし
  • docコメントブロックから普通のコメントブロックにしてしまえばテストも実行されない

HamcrestのassertThatだけを使う

PHPUnitのように、テストクラスを書く必要があるもので上記実装するのは超大変なので、HamcrestというMatcherライブラリのPHP版を使ってみます。

グローバル関数でassertThatが用意されているので、これを使います。assertThatはassertEqualsよりも直感的に英文に近い形で書くことができ、エラー時の表示もわかりやすいのが特徴です。

see also

チュートリアルにもあるようにMatcherライブラリなので、PHPUnitと組み合わせて使うこともできます。

hamcrestを使うと "$this->assertThat" と毎回 "$this->" を書かないといけないところを"assertThat"とグローバル関数で実行することができます。*1

Hamcrester

今回書いたのは、Hamcrestをコードのdocコメントブロックで書くことができるHamcresterです。

そんなにたいしたことはしてないのでgistにコードをコピペっておきました。

まず、vendor以下にHamcrestのライブラリを展開しておきます。

/Project
├── src
│      * sample.php ... ソースコード
└── vendor
    └── Hamcrest-1.1.0
        └── Hamcrest
               * Hamcrest.php

次にHamcresterをgistからとってきます。

$ wget https://gist.github.com/brtriver/5179397/raw/5bbe577769e079519901d3ba02d4f47ba892f2f9/hamcrester.php

最初のアイデアとして@assertThatと書いてましたが、それすらも長いので@tアノテーションで書けるようにします。

そして、できたのが以下のようなサンプル

  • sample.php *2
<?php
$users = [
    '太郎' => 'male',
    '花子' => 'female',
    '一郎' => 'male',
];
// この$usersから男性(male)のものだけを抽出するfilterMale関数を作りたい

/**
 * @t(filterMale($users), is(arrayValue()));
 * @t(filterMale($users), not(hasValue('female')));
 * @t(filterMale($users), is(arrayWithSize(2)));
 */

function filterMale($users)
{
    return array_filter($users, function($user){
            return ($user === 'male')? true: false;
        });
}

// 女性だけ抽出する filterFemale関数を作りたい

/**
 * @t(filterFemale($users), is(arrayValue()));
 * @t(filterFemale($users), not(hasValue('male')));
 * @t(filterFemale($users), is(arrayWithSize(1)));
 */

function filterFemale($users)
{
    return array_filter($users, function($user){
            return ($user === 'female')? true: false;
        });
}

あとは、これを実行するだけ。

  • 失敗時
% php hamcrester.php src/sample.php
exception 'Hamcrest_AssertionError' with message 'Expected: is ["female"]
     but: was ["male", "female", "male"]' in ....
  • 成功時
% php hamcrester.php src/sample.php
OK (All Tests Green)

おー。できたできた。

実際は色付きで出力してるので以下なかんじ。

f:id:brtRiver:20130317145007p:image:w640

動きとしてはテスト実行時は、docコメントを外して @t を assertThatとして動かしているだけです。

PHPらしく、ちょっとしたコードをトライアンドエラーで書き上げていくときにこういうテストを使った実装方法があってもいいですよね。

Guardを使ってファイルの更新を監視してテストを実行する

次にやりたくなるのが、毎回テストを実行するのが面倒なので、更新時に自動的に実行してほしいという要望ですよね。

そこで、Guardというgemを使ったりするといいと思います。

サンプルはgistにおいてあるので、参考にしてみてください

https://gist.github.com/brtriver/5179397


(追記)

"それなんてDocTest..."

ブクマで指摘いただいてますが、そうです。やりたいのはDocTestです。

ただ欲しいのはassetThatを実行してくれるだけだったりします。


Phakeを使ったり、Hamcrest使ったりと、なんとなく自分の中でPHPUnitへの依存がどんどん減っていってる...

*1:PHPUnitのassertThatよりも書きやすいし、これだけでも使ってみると良いと思いますし、実際自分もとあるプロジェクトの全アサーションをHamcrestのassertThatに書き換えました。ただし、見てのとおり短い名前のグローバル関数を利用するのでそれなりの考慮が必要です

*2:追記: テストデータが含まれてるパターンだったので、テストデータがないパターンに修正

2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2013 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2014 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2015 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2016 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |