こんにちは。はじめまして。tarokamikazeです。
PHPUnit を書いていると、何度も同じ処理内容やAssertが出てくることはありませんか?
テストが冗長で、いやになっちゃいますね。
よろしい、ならばリファクタだ。
冗長でいやになる例
/**
* REST API 用のテストクラスがいっぱいあると思ってください
*/
class RestApiTest extends \PHPUnit_Framework_TestCase
{
public function test_getFoo()
{
// jsonで出力されて欲しいキー
$expectedColumns = ['id', 'name', 'foo'];
$json = ...// コントローラーを叩いて、jsonを取り出す
/** ここから共通処理 **/
// jsonを配列にdecodeして、キーが一致するか確認する
$data = json_decode($json, true);
$this->assertTrue(is_array($data));
$keys = array_keys($data);
// json hash は本質的にカラム順を保証しないので
// ソートしたうえでキーの一致チェックをしないとダメ
sort($keys);
sort($expectedColumns);
$this->assertEquals($expectedColumns, $keys);
/** ここまで共通処理 **/
}
}
ひと目でなにをやっているのかわかります。
が、この先コピペ地獄が待っていますね。
共通処理を基底クラスに逃がしてみる
誰でも思いつく手は「extendsすればいーじゃん」というもの。
/**
* 共通テスト用基底クラス
*/
abstract class BaseRestApiTest extends \PHPUnit_Framework_TestCase
{
public static function assertJsonColumns(array $expectedColumns, $json)
{
// 配列にdecodeして、キーが一致するか確認する
$data = json_decode($json, true);
self::assertTrue(is_array($data));
$keys = array_keys($data);
sort($keys); // json hash は本質的にカラム順の保証がないので、ソートしたうえでキーの一致チェックをしないとダメ
sort($expectedColumns);
self::assertEquals($expectedColumns, $keys);
}
}
class RestApiTest extends BaseRestApiTest
{
public function test_getFoo()
{
// jsonで出力されて欲しいキー
$expectedColumns = ['id', 'name', 'foo'];
$json = ...// コントローラーを叩いて、jsonを取り出す
// 基底クラスに逃がした共通処理を叩く
$this->assertJsonColumns($expectedColumns, $json);
}
}
これはこれは。お手軽ですね。
しかし、このノリで複数の基底クラスを作っていってしまうと
迷子になったり多重継承問題に突き当たったり基底クラスが肥大化したりと
いいことはありません。
オレオレアサーションクラスを作ってみる
実は、Assertするだけのクラスを作ることもできるんです。
個人的には一番好きなやり方。
/**
* アサーション専用クラス。
* テスト本体でもないクラスがTestCaseを継承するのがキモいので
* PHPUnit_Framework_Assertを継承する。
*/
class JsonColumnAssert extends \PHPUnit_Framework_Assert
{
public static function assert(array $expectedColumns, $json)
{
// 配列にdecodeして、キーが一致するか確認する
$data = json_decode($json, true);
self::assertTrue(is_array($data));
$keys = array_keys($data);
sort($keys); // json hash は本質的にカラム順の保証がないので、ソートしたうえでキーの一致チェックをしないとダメ
sort($expectedColumns);
self::assertEquals($expectedColumns, $keys);
}
}
class RestApiTest extends BaseRestApiTest
{
public function test_getFoo()
{
// jsonで出力されて欲しいキー
$expectedColumns = ['id', 'name', 'foo'];
$json = ...// コントローラーを叩いて、jsonを取り出す
// アサーション専用クラスに逃がした共通処理を叩く
JsonColumnAssert::assert($expectedColumns, $json);
}
}
単一責任原則です。いいかんじ。
あまり見ない形なので、見慣れないひとは迷うかも。でも慣れると快感。
おまけ: オレオレアサーションクラスを潔癖に作ろうとして心が折れた
オレはextendsとかキライなんだよ!という潔癖なお方のための
何にも依存しないテストはこちら。
そもそも「テスト失敗」をPHPUnitはどう表現しているのでしょうか?
ソースを見る限り、PHPUnit_Framework_AssertionFailedError を
投げているに過ぎません。
// vendor/phpunit/phpunit/src/Framework/Assert.php
abstract class PHPUnit_Framework_Assert
{
/** 略 **/
public static function fail($message = '')
{
throw new PHPUnit_Framework_AssertionFailedError($message);
}
/** 略 **/
}
ということは、オレオレアサーションで、このエラーを投げれば
失敗と判断してくれる、ということです!
/**
* アサーション専用クラス。
* なにもextendsしない!
*/
class JsonColumnAssert
{
public static function assert(array $expectedColumns, $json)
{
// 配列にdecodeして、キーが一致するか確認する
$data = json_decode($json, true);
if(!is_array($data)){
throw new \PHPUnit_Framework_AssertionFailedError('jsonじゃないですよ');
}
$keys = array_keys($data);
sort($keys); // json hash は本質的にカラム順の保証がないので、ソートしたうえでキーの一致チェックをしないとダメ
sort($expectedColumns);
if($expectedColumns !== $keys){
throw new \PHPUnit_Framework_AssertionFailedError('一致しませんよ');
}
}
}
class RestApiTest extends BaseRestApiTest
{
public function test_getFoo()
{
// jsonで出力されて欲しいキー
$expectedColumns = ['id', 'name', 'foo'];
$json = ...// コントローラーを叩いて、jsonを取り出す
// アサーション専用クラスに逃がした共通処理を叩く
JsonColumnAssert::assert($expectedColumns, $json);
}
}
これだと、失敗時はいい感じにfail になってくれます!
しかし...
PHPUnit 5.2.12 by Sebastian Bergmann and contributors.
一致しませんよ
***/RestApiTest.php:21
***/RestApiTest.php:44
Time: 56 ms, Memory: 6.00Mb
FAILURES!
Tests: 1, Assertions: 0, Failures: 1.
Assertions: 0...だと...?
そう、Assertionを数える機能が効きません。
Assertionを数える機能は(おそらく)ここでやってます。
// vendor/phpunit/phpunit/src/Framework/Assert.php
abstract class PHPUnit_Framework_Assert
{
private static $count = 0;
/** 略 **/
public static function assertThat($value, PHPUnit_Framework_Constraint $constraint, $message = '')
{
self::$count += count($constraint);
$constraint->evaluate($value, $message);
}
/** 略 **/
}
このassertThatは、すべてのassert***メソッドを叩くときに通るものです。
countを足し上げてますね。
phpunitコマンドを叩いた時の「Assertions」は、ここを参照しているものと予想されます。
しかしprivate staticなプロパティなので、こいつを外部からいじるのは一苦労ですね。
それだったら、素直にPHPUnit_Framework_Assertを継承したほうが
楽で読みやすいと思います。
備考
1テストメソッドにいくつもassertを書くな教団があるらしいですね。
わたしはどちらでもいいよ派です。