読者です 読者をやめる 読者になる 読者になる

PHPUnitを便利にするライブラリ幾つかつくった

年末年始+αの時間で PHPUnit をちょっと便利にするライブラリを幾つか作ったのでそれぞれ書いておく。

assert_chain

https://github.com/gong023/assert_chain

PHPUnit で一つの変数について詳しくアサーションをかけたい場合、以下のように書くことがあると思う。

<?php
$arr = [
    'intKey'    => 1,
    'stringKey' => 'foo',
    'boolKey'   => true,
];

$this->assertNotEmpty($arr);
$this->assertArrayHasKey('intKey', $arr);
$this->assertSame(1, $arr['intKey']);
$this->assertArrayHasKey('stringKey', $arr);
$this->assertSame('foo', $arr['stringKey']);
$this->assertArrayHasKey('boolKey', $arr);
$this->assertTrue($arr['boolKey']);

一体何度 $this->assert を何回書けばいいんだという話になるので、assert_chain を使って以下のように書けるようにした。以下は上記のコードと等価だ。

<?php

$this->assert()
  ->notEmpty($arr)
  ->arrayHasKey('intKey', $arr)
  ->same(1, $arr['intKey'])
  ->arrayHasKey('stringKey', $arr)
  ->same('foo', $arr['stringKey'])
  ->arrayHasKey('boolKey', $arr)
  ->true($arr['boolKey']);

また、PHPUnit の assert はほぼ全て $actual の値(つまりテスト対象の変数)を渡す部分があるが、centralizedAssert を使うことでテスト対象の変数が自動的にアサーションに渡るようになっている。

<?php

$arr = ['key' => 'value'];

$this->centralizedAssert($arr)
  ->notNull()
  ->notEmpty()
  ->notCount(0)
  ->count(1)
  ->arrayNotHasKey('no existing key')
  ->arrayHasKey('key')
  ->notContains('no existing value')
  ->contains('value')
  ->equals(['key' => 'value']);

アサーションPHPUnit_Framework_Assert にプロキシしてるだけなので、普段 PHPUnit を使っている人は違和感なく使えるはず。当然ロジックも変わらない。

詳しいことは README を見て欲しい。

assertEqualsassertSame を使って一度に雑にテストするのもいいが、このように複数アサーションを重ねて段階的にテストしていくとテストが失敗した時に原因がわかりやすい。

Ayaml

https://github.com/gong023/Ayaml

yml による fixture ライブラリ。yml ファイルから php array を生成する。

# user.yaml
valid_user:
  id: 1
  name: Taro
  created: 2014-01

上記ファイルから以下のように array を作る。

<?php
Ayaml::file('user')->schema('valid_user')->dump();
=> ['id' => 1, 'name' => 'Taro', 'created' => '2014-01'];

with メソッドを使って値の上書きもできる。

<?php
Ayaml::file('user')->schema('valid_user')->with(['id' => 2, 'name' => 'John'])->dump();
=> ['id' => 2, 'name' => 'John', 'created' => '2014-01'];

以下のようにシーケンシャルな配列を作ることもできる。

<?php
$validUser = Ayaml::file('user')->schema('valid_user');

Ayaml::seq($validUser)->range('id', 10, 12)->byOne()->dump();
=>
[
  ['id' => 10, 'name' => 'Taro', 'created' => '2014-01'],
  ['id' => 11, 'name' => 'Taro', 'created' => '2014-01'],
  ['id' => 12, 'name' => 'Taro', 'created' => '2014-01'],
];

Ayaml::seq($validUser)->between('created', '2014-01', '2014-03')->byMonth()->dump();
=>
[
  ['id' => 1, 'name' => 'Taro', 'created' => '2014-01'],
  ['id' => 1, 'name' => 'Taro', 'created' => '2014-02'],
  ['id' => 1, 'name' => 'Taro', 'created' => '2014-03'],
];

詳しいことは README を見て欲しい。

Ayaml の特徴は、テストデータそのものを DB データとして作るのではなく、単純に array を生成する点。

php の fixture ライブラリとして有名なものとして、DBUnit, alice, php-factory-girl がある。これらのライブラリは、Ayaml とは違い DB にデータを入れるところまで責任を持ってくれる。

もちろんそれはそれで良いのだが、一方で DB 周りを扱うライブラリは馴染みのあるものを使いたいという気持ちはないだろうか。少なくとも自分にはある。上記のライブラリは doctrine を使うことになるので、テストのためにわざわざその DSL を覚える羽目になる(DBUnit はそうとも限らないが、あれはあれでよく我慢して使えるなと思ってる)。正直それは馬鹿げているし、あーシャーディングされてる場合どうすんだっけとか考えだすとテストデータひとつ作るのに膨大な時間を費やす羽目になる。

Ayaml は DB にデータを入れるという責任を放棄し、手前で好きに array 作って手前のライブラリに流し込めというスタンスにすることで自由度を上げている。php には active_record のような支配的なライブラリがないので、こういうアプローチもありだと考えた。

ただ一方で、Ayaml を実装する際自分に課した制約として「doctrine の DSL を覚えるより短い時間で実装すること」というのがあり(それ以上かかるなら既存ライブラリ使ったほうが早いから)、実際 ver0.1.0 までは 1h 弱ぐらいの時間で実装したので、特にシーケンシャル array を作る所なんかはパワー不足な面が否めない。 Ayaml 以上に仕事してくれる DSL が欲しいとか、プロダクトで doctrine を使っているとかいう事情があれば、alice を検討するのがよいと思う。

PHPUnityo

https://github.com/gong023/phpunityo

PHPUnit の成功時 or 失敗時に Yo してくれる。travis でも使える。

まぁ自分は CI の結果は普通にメールで見てるのでぶっちゃけそんなに必要ないのだが、Yo で送られてくるとめでたい。それだけ。

設定は Yo の API token を環境変数に突っ込んだ上で phpunit.xml.dist に以下の情報を足せばいい。

<listeners>
  <listener class="PHPUnit_Yo" file="vendor/gong023/src/PHPUnit_Yo.php">
    <arguments>
      <string name="sendUser">Your Name</string>
      <string name="onSuccess">false</string>
      <string name="onFailure">true</string>
    </arguments>
  </listener>
</listeners>

詳しくは README を見て欲しい。