PHP Paiza 自動テストテンプレート

以前書いた [PHP Paiza 標準入力テンプレート](https://qiita.com/ucan-lab/items/6a3c1a2923ba30824f39) の記事が好評でPaizaの問題を解くときに参考にしてますというお声をよくいただいてます。とても嬉しいです☺️

## 環境

- PHP: 7.3以上

## お悩み相談

Paizaに問題を提出すると標準入力と標準出力の結果を確認されるのですが、毎回手でポチポチ入力例を入力していくのは手間ですよね...？
入力例が1行であれば、まぁいいかとなるんですが、複数行入力するのは割と時間かかっちゃいますし、誤入力やりがちです。

[phpunit](https://phpunit.readthedocs.io) というPHPの便利なテストフレームワークはありますが、Paizaの問題を解くためにフレームワークを入れるのは、まぁ大掛かりな気持ちがあります。

ということで今回は自前のテストコードを作っていきたいと思います。

## FizzBuzz問題

今回は[レベルアップ問題集 | Paiza](https://paiza.jp/works/mondai)から[Fizz Buzz (paizaランク C 相当) | Paiza](https://paiza.jp/works/mondai/skillcheck_sample/fizz-buzz?language_uid=php)を解いていきます。

整数 N が入力として与えられます。

1からNまでの整数を1から順に表示してください。

ただし、表示しようとしている数値が、

・3の倍数かつ5の倍数のときには、"Fizz Buzz"
・3の倍数のときには、"Fizz"
・5の倍数のときには、"Buzz"

を数値の代わりに表示してください。

```
入力例1
5

出力例1
1
2
Fizz
4
Buzz
```

```
入力例2
20

出力例2
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
Fizz Buzz
16
17
Fizz
19
Buzz
```

## FizzBuzz解答例

色々な解き方はあるかと思いますが、私の解答例です。

```php:fizzbuzz.php
<?php declare(strict_types=1);

$input = trim(fgets(STDIN));

for ($i = 1; $i <= $input; $i++) {
    echo fizzBuzz($i), PHP_EOL;
}

function fizzBuzz(int $num): string
{
    return [
        [(string) $num, 'Buzz'],
        ['Fizz', 'Fizz Buzz'],
    ][$num % 3 == 0][$num % 5 == 0];
}
```

久しぶりにFizzBuzz問題を解きましたが、難しいですね。
何とかかんとか解けましたが、果たしてこのコードは正しいのでしょうか...？
自信がありません...。自動テストさえできれば...

## 本題: テストコードを用意する

`test.php` を用意します。

```php:test.php
<?php declare(strict_types=1);

// テストしたいファイルを指定する
$command = 'fizzbuzz.php';

// テストケースを定義する
$patterns = [
    'case1' => [
        'stdin' => ['5'],
        'stdout' => ['1', '2', 'Fizz', '4', 'Buzz'],
    ],
    'case2' => [
        'stdin' => ['20'],
        'stdout' => explode(PHP_EOL, <<<EOD
            1
            2
            Fizz
            4
            Buzz
            Fizz
            7
            8
            Fizz
            Buzz
            11
            Fizz
            13
            14
            Fizz Buzz
            16
            17
            Fizz
            19
            Buzz
            EOD
        ),
    ],
    'case3' => [
        'stdin' => ['3'],
        'stdout' => ['1', '2', 'Fuga'], // わざとテストが落ちるデータを入れてます
    ],
];

foreach ($patterns as $case => $pattern) {
    assertStandard($command, $case, $pattern['stdin'], $pattern['stdout']);
}

function assertStandard(string $command, string $case, array $stdin, array $stdout): void
{
    $args = implode('\\\n', $stdin);
    $actual = trim(`echo $args | php $command`);
    $expected = implode(PHP_EOL, $stdout);

    echo sprintf("%s: %s, stdin: ['%s'], stdout: ['%s'] ... ", $command, $case, implode("', '", $stdin), implode("', '", $stdout));

    try {
        assert($expected === $actual);
        echo 'ok', PHP_EOL;
    } catch (AssertionError $exception) {
        echo 'ng', PHP_EOL, '[actual]', PHP_EOL, $actual, PHP_EOL;
        exit;
    }
}
```

標準入力と標準出力をテストするため、 `echo $args | php $command` でphpコード内でphpを実行してます。
バッククォート([実行演算子](https://www.php.net/manual/ja/language.operators.execution.php))で囲むと[shell_exec](https://www.php.net/manual/ja/function.shell-exec.php)と等価です。

`$actual` には実際のコードの実行結果、`$expected`にはテストケースに定義した予期する値を設定してます。
[`assert`](https://www.php.net/manual/ja/function.assert.php)メソッドで結果を比較してます。

実際に使う場合は、テストしたいファイル名とテストケースを定義して使ってください。
テストケースは配列で定義する必要があります。標準入力や標準出力の数が多い場合は例のように[ヒアドキュメント](https://www.php.net/manual/ja/language.types.string.php#language.types.string.syntax.heredoc)を使って定義してください。

テストケースは別ファイルから読み込むようにするか悩みましたが、どうだろう...？ またご要望あれば検討しようかなと思います。

## 自動テストを実行する

```shell-session
$ php test.php
fizzbuzz.php: case1, stdin: ['5'], stdout: ['1', '2', 'Fizz', '4', 'Buzz'] ... ok
fizzbuzz.php: case2, stdin: ['20'], stdout: ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', 'Fizz', '13', '14', 'Fizz Buzz', '16', '17', 'Fizz', '19', 'Buzz'] ... ok
fizzbuzz.php: case3, stdin: ['3'], stdout: ['1', '2', 'Fuga'] ... ng
[actual]
1
2
Fizz
```

テストケース1, 2はちゃんと通ってるのでひと安心ですね☺️
テストケース3はあえて落ちるようにしています。

テストが通った場合は `ok`
テストが通らなかった場合は `ng` と実行結果を返すようにしてます。

これでPaizaの問題を解くときの動作確認がラクになるかなと思います！
もしよかったら使ってみてください！

## 参考

- [PHP7.3からヒアドキュメントのインデントのズレが解消される件](https://qiita.com/a-nishimura/items/9f88e9aeacf6fc4d9e4c)
- [UnitTest の Expected と Actual の意味](https://qiita.com/deflis/items/645451d7fd85932d1134)
- [PHP で複数の値を連結して echo する時のちょっとした tips](https://qiita.com/trashtoy/items/5c76f3cd8cb636ac3c72)