PHPのテストで本当にdataProviderを使う必要があるのだろうか

PHPUnitにはdataProviderという機能がある。
パラメタライズドテスト、またはテーブルドリブン(駆動)テストで書くための仕組み。

僕はずっとこの機能が使いづらくて、普通にforeachで回す方が良いのではと思っている。
今日たまたまdataProviderを推奨している記事を続けて目にしたので、改めて考えを整理しておく。

dataProviderのメリット

foreachと比較したdataProviderのメリットはこちらの記事が分かりやすかったので引用する。
PHPUnitとデータプロバイダとテストケース生成

メリット1: setUp/tearDownが使える

確かにモックを使うテストならその方が助かる。

メリット2: どのデータでテストが落ちたのか分かりやすい

確かにforeachだとちょっと面倒。Goでテストを書いている時にt.Error()をいちいち書いていく感覚に似てる。

メリット3: 例外のテストとの組み合わせ

確かに例外が発生した段階で止まっちゃうと面倒。

dataProviderの(個人的)デメリット

上記のメリットはどれも同意するけど、個人的にはそれ以上にデメリットを感じてしまう。
それはテストの本体とdataProviderのメソッドが離れていること。ロジックとデータは近いところにある方が読みやすいと思うけど好みなのかもしれない。

例えばPHPUnitリポジトリにあるこのテストコード。
https://github.com/sebastianbergmann/phpunit/blob/cca308e970eebb1f21cd702071b09bfd40a1c705/tests/unit/Util/RegularExpressionTest.php

  1. testValidRegexのテストを読もうとする
  2. validRegexpProviderの定義を見る

という視線の移動と記憶が必要になる。
これなら↓のようにテスト本体とデータは一緒にあった方が個人的には読みやすい。

<?php
public function testValidRegex(): void
{
    foreach([
        ['#valid regexp#', 'valid regexp', 1],
        [';val.*xp;', 'valid regexp', 1],
        ['/val.*xp/i', 'VALID REGEXP', 1],
        ['/a val.*p/', 'valid regexp', 0],
    ] as $case) {
        $this->assertEquals($case[2], RegularExpression::safeMatch($case[1], $case[0]));
    }
}

(エラーメッセージとか一時変数に置くとかは省いている)

もちろんdataProviderの方が適切というケースもあると思うけど、同様に普通にforeachの方が良いケースもあると思う。何でもかんでもdataProviderを使うのは違う気がする。
このテストとか冗長なだけなのでは。
ただ「使うかどうかは個人の判断」にしちゃうとテストの書き方に統一性がなくなるから、プロジェクトとしては「必ず使う」でルール化した方がいいのかもしれない。

また個人的に読みにくいと感じる理由の一つは、dataProvider側だけを見ても値の意図が分からないから。

<?php
public function canonicalizeProvider(): array
{
    return [
        ['{"name":"John","age":"35"}', '{"age":"35","name":"John"}', false],
        ['{"name":"John","age":"35","kids":[{"name":"Petr","age":"5"}]}', '{"age":"35","kids":[{"age":"5","name":"Petr"}],"name":"John"}', false],
        ['"name":"John","age":"35"}', '{"age":"35","name":"John"}', true],
    ];
}

これはphpunit/tests/unit/Util/JsonTest.phpから拝借した。
それぞれの値の意図は、ここを見ただけでは分からない。
テストコード側の引数を見て初めて理解する。

<?php
public function testCanonicalize($actual, $expected, $expectError): void

テストコードとデータを行き来しないと読み解けない。これが僕にはつらい。

結局どっちが良いのか

最初に引用したdataProviderのメリットのうち2つは、「テストが落ちた時のメンテ性を上げる」ためのものだった。
僕があげたdataProviderのデメリットは「他人がコードを読む時」を想定したものだった。

つまり優劣というより重要視する観点が違うんだと思う。僕は可読性を大事にしている。でも「dataProviderの方が見やすい。foreachの方が見づらい」って人もいるだろうから、そうなるとforeachの方が良い理由はほとんどないのかな。