PHP版の Power Assert もどき「Phpower」

js には Power Assert という便利なパッケージがあります。私自身は js でテストをほとんど書いたことがないので使ったこと無いのですが、例えば次のようなアサーションが、

assert(ary.indexOf(zero) === two)

次のように assert の各部位の値が表示されるようになります(README より)。

  assert(ary.indexOf(zero) === two)
         |   |       |     |   |
         |   |       |     |   2
         |   -1      0     false
         [1,2,3]

  [number] two
  => 2
  [number] ary.indexOf(zero)
  => -1

通常のアサーションライブラリでは大量のアサーション API があり、わかりやすい出力を得るためには API を使い分ける必要があります。Power Assert ならだいたい assert ひとつで上記のようなわかりやすい出力が得られるため、大量の API を覚える必要が無いメリットがあります。

これはソースコードの AST を書き換えて assert を拡張することで実現されているのですが、今日日の PHP ならソースコードを解析して AST を得るのは容易なので、同じようなものを PHP で作ってみました。

Phpower

Composer でインストールできます。重要な点として PHPUnit も Composer でインストールする必要があります。

composer require --dev phpunit/phpunit ngyuki/phpower:dev-master

例えば次のようなテストコードが、

<?php
namespace Test;

use PHPUnit\Framework\TestCase;

class SomeTest extends TestCase
{
    public function test()
    {
        $ary = [1,2,3];
        $zero = 0;
        $two = 2;
        assert(array_search($zero, $ary, true) === $two);
    }
}

次のように出力されます。

Assertion failed array_search($zero,$ary,true) === $two -> false
# $zero -> 0
# $ary -> [
#           1,
#           2,
#           3,
#         ]
# array_search($zero,$ary,true) -> false
# $two -> 2

Power Assert のように罫線を引きたいところですが簡単ではなさそうなので上のような出力になっています。値の表示には symfony/var-dumper を使っています。配列が要素ごとに改行されるので縦幅がちょっと冗長な気もしますね。

いくつか制限もあります。今判っている限りでは次のような制限があります。

phpunit.phar は未サポート

PHPUnit はテストコードを読み込むときに PHPUnit\Util\FileLoader というクラスを使っています。PHPUnit がこのクラスをロードする前に同名のクラスを先に定義することで PHPUnit\Util\FileLoader の実装を差し替えて、テストコードが読み込まれるときにコードの書き換えを行っています。

PHPUnit\Util\FileLoader の差し替え版の読み込みは composer.jsonautoload.files で行うようにしているので vendor/autoload.php が読まれた時点で差し替えが行われます。Composer で入れた PHPUnit であれば vendor/bin/phpunit の中で一番最初に vendor/autoload.php が読まれるのでこれは有効に働きます。

しかし、phpunit.phar の場合、phpunit.phar のスタブコードで phpunit.phar に含まれるコードが一括で読まれています。そこに PHPUnit\Util\FileLoader も含まれているため差し替えることができず、テストコードを書き換えることができません。

ので、phpunit.phar で実行した場合は assert はただの assert として機能します。

リファレンス引数の関数呼び出し

リファレンス引数を持つ関数呼び出しが含まれると元の assert と互換性がなくなります。

例えば次のようなコードです。

function f(&$r)
{
    return ++$r;
}
$a = 0;
assert(f($a)); // Notice: Only variables should be passed by reference
assert($a === 1);

これは assert の中身を次のように書き換えているためです(簡略化しています、実際のものとは異なります)。

_expr(_cap(f(_cap($a))))

f(_cap($a)) のように、リファレンス引数であるはずの f 関数の引数が _cap 関数の戻り値になるため Only variables should be passed by reference となり、関数に $a の参照が渡らなくなります。

さいごに

assert だけで式の中の途中経過まですべて表示されるのは便利だと思う反面、PHPUnit の assertSame などに配列を渡せば expected と actual の diff が表示されるので、どこが間違っているか直ぐに把握できて便利です。

Phpower だと差分にはならないし、また、情報量も無駄に多くなりがちなので、どこが間違っているかをぱっと見で把握するのはなかなか困難です。

ので、アサーションを全部置き換えるようなことはできず、結局使い分けは必要でしょう。もっとも、まだ作ったばかりでどの程度使えそうか自分でも判らないので、とりあえずどこかのプロジェクトに突っ込んでみて使用感を見つつ改善などしていこうと思います。