PHPUnitをマチカネタンホイザにする

## ことの発端

https://twitter.com/tanakahisateru/status/1497441503165448197

↑を見て、『あー、できるかもなー』と思ったのでやってみました。

## --printerオプションで出力をカスタマイズできる

PHPUnitには`--printer`オプションがあります。[ドキュメント](https://phpunit.readthedocs.io/ja/latest/textui.html?highlight=printer#textui-clioptions)には

```
結果を表示するために使うプリンタクラスを指定します。このプリンタクラスは PHPUnit\Util\Printer を継承し、　　
かつPHPUnit\Framework\TestListener インターフェイスを実装したものでなければなりません。
```

と記載があります。独自のプリンタークラスを作ればいけそうです。ソースを追っていくとデフォルトでは、`PHPUnit\TextUI\DefaultResultPrinter`というクラスを使っているようです。このクラスを拡張したクラスを作ることで、マチカネタンホイザなPHPUnitを作れそうです。

## 独自プリンタークラス、MatikanetannhauserPrinterを作る

とりあえず、テストに失敗（F）したら`むん！`、テスト成功（.）したら`えい、`にしてみたいと思います。上記の通り`PHPUnit\TextUI\DefaultResultPrinter`の拡張クラスを作りますが、中のコードを見ると、

```php:DefaultResultPrinter.php
    /**
     * A failure occurred.
     */
    public function addFailure(Test $test, AssertionFailedError $e, float $time): void
    {
        $this->writeProgressWithColor('bg-red, fg-white', 'F'); // 失敗した時の出力
        $this->lastTestFailed = true;
    }

    /**
     * A test ended.
     */
    public function endTest(Test $test, float $time): void
    {
        if ($this->debug) {
            $this->write(
                sprintf(
                    "Test '%s' ended\n",
                    \PHPUnit\Util\Test::describeAsString($test)
                )
            );
        }

        if (!$this->lastTestFailed) {
            $this->writeProgress('.'); // 成功した時の出力
        }

        if ($test instanceof TestCase) {
            $this->numAssertions += $test->getNumAssertions();
        } elseif ($test instanceof PhptTestCase) {
            ++$this->numAssertions;
        }

        $this->lastTestFailed = false;

        if ($test instanceof TestCase && !$test->hasExpectationOnOutput()) {
            $this->write($test->getActualOutput());
        }
    }
```

となっており、`addFailure`メソッドで失敗時の出力、`endTest`でテストが成功している時に成功時の出力をしています。このメソッドをオーバーライドしてしまえば、さくっと作れそうです。

```php:tests/MatikanetannhauserPrinter.php
<?php

namespace App\Tests;

use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\TextUI\DefaultResultPrinter;
use Throwable;

class MatikanetannhauserPrinter extends DefaultResultPrinter
{
    /**
     * A failure occurred.
     */
    public function addFailure(Test $test, AssertionFailedError $e, float $time): void
    {
        $this->writeProgress('むん！ ');
        $this->lastTestFailed = true;
    }

    /**
     * A test ended.
     */
    public function endTest(Test $test, float $time): void
    {
        if ($this->debug) {
            $this->write(
                sprintf(
                    "Test '%s' ended\n",
                    \PHPUnit\Util\Test::describeAsString($test)
                )
            );
        }

        if (!$this->lastTestFailed) {
            $this->writeProgress('えい、');
        }

        if ($test instanceof TestCase) {
            $this->numAssertions += $test->getNumAssertions();
        } elseif ($test instanceof PhptTestCase) {
            ++$this->numAssertions;
        }

        $this->lastTestFailed = false;

        if ($test instanceof TestCase && !$test->hasExpectationOnOutput()) {
            $this->write($test->getActualOutput());
        }
    }
}
```

これでプリンタークラスは完成しました。今回、赤文字で失敗表示されるのがちょっと微妙だったので、`writeProgressWithColor`から`writeProgress`に変更しました。なお、composer.jsonの`autoload-dev`にtestsディレクトリが`App\Tests`になるようにオートロード設定しています。

## プリンターを設定してマチカネタンホイザにする

独自プリンタークラスができたので、`vendor/bin/phpunit --printer "App\Tests\MatikanetannhauserPrinter"`と実行すればマチカネタンホイザになりますが、毎回プリンター指定はめんどくさいので、設定を変更して独自プリンターを使うように変更します。

```diff:phpunit.dist.xml
<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="tests/bootstrap.php"
-         convertDeprecationsToExceptions="false">
+         convertDeprecationsToExceptions="false"
+         printerClass="App\Tests\MatikanetannhauserPrinter">
<!-- 以下略 -->
```

`phpunit`タグに`printerClass`属性を設定することで、デフォルトでこのクラスを使うようになります。


では実行してみましょう。適当にテストクラスを作ります。

```php:tests/SomeTest.php
<?php

namespace App\Tests;

use PHPUnit\Framework\TestCase;

class SomeTest extends TestCase
{
    public function testHoge()
    {
        $this->assertTrue(true);
    }

    public function testFoo()
    {
        $this->assertTrue(true);
    }

    public function testBuzz()
    {
        // むん！にしたいのでわざと失敗させます。
        $this->assertFalse(true);
    }
}
```

あとは、いつも通りコマンドを叩きます。

```shell
% vendor/bin/phpunit
Cannot load Xdebug - it was already loaded
PHPUnit 9.5.16 by Sebastian Bergmann and contributors.

えい、えい、むん！                                                                  3 / 3 (100%)

Time: 00:00.011, Memory: 6.00 MB

There was 1 failure:

1) App\Tests\SomeTest::testBuzz
Failed asserting that true is false.

/Users/user/Project/tests/SomeTest.php:21

FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
```

はい。できました。