PHPUnit のデータプロバイダでテストに必要なあらゆるデータをプロバイドする


この記事について

PHPUnit を使ってテストを書くとき、テストケースごとにデータの前準備やアサーションが異なる場合があります。
テストメソッド自体を分ける、という方法でもちろんいいんですが、そこまでじゃないんだよなぁ、というような場合に、それらをすべてデータプロバイダを使ってテストメソッドに渡してやればいいんじゃない、と思ったのでやってみました。

はじめに

PHPUnit を使ったテストには、大きく分けて以下のものが必要です。

  • 前準備
  • 入力パラメータ
  • テスト対象の処理実行
  • 出力データ
  • アサーション

前準備は、 setUp() メソッドの中とか、テストメソッドの先頭でやりますが、テストケースによっていくつかパターンがあったりするとテストメソッド内で条件分岐したりして見通しが悪くなります。また、アサーションも同様で、テスト対象のメソッドを実行したあとに、ずらずらとアサーションメソッドが続くとよく分からないかんじになってしまいます。

なので、テストメソッドが以下のようなかんじのスッキリした構造になるようにしたいです。

public function testSomething($preparation, $parameters, $assertion)
{
    $preparation();
    $testTarget = new TestTarget();
    $actual = $testTarget->something($parameters);
    $assertion($actual);
}

実装

データプロバイダ

public function dataSomething()
{
    $preparations = $assertions = [];

    // これをパターン分用意する、共通部分は変数に切り出す
    // 長くなるようなら別メソッドに切り出す
    $preparations['success'] = function (array $dependencies) {
        $someModel = factory(SomeModel::class)->create();
        $someModel->someAction($dependencies['value']);
        return ['someModel' => $someModel];
    };

    // こっちはパターンに依存する値を渡せるようにした
    // これも長くなるようなら別メソッドに切り出す
    $assertions['success'] = function ($value) {
        return function ($actual) use ($value) {
            $this->assertSame($value, $actual);
        };
    };

    return [
        'success.pattern-01' => [
            'preparation' => $preparations['success'],
            'parameters'  => [...],
            'assertion'   => $assertions['success']('parameter-01'),
        ],
    ];
}

上の例では、$preparation の方は、Laravel のモデルファクトリを使ってテスト実行に必要なデータベースの状態を準備するためのクロージャです。いくつかのテストケースで分類できる(成功と失敗の2パターンとか)のであればベタ書きでパターン分用意してもいいかもしれません。

$assertion の方は、テストケースごとにパラメータを渡せるように、クロージャを返すクロージャにしたパターンです。

状況に応じて使い分けるといいんじゃないかと思います。

テストメソッド

public function testSomething($preparation, $parameters, $assertion)
{
    $commonData ['value' => ...];
    $preparedData = $preparation($commonData);
    $testTarget = new TestTarget($preparedData);
    $actual = $testTarget->doSomething($parameters);
    $assertion($actual);
}

というかんじでできました。

おわりに

いかがだったでしょうか、なにをテストしているのかがパッと見て分かるように、テストメソッド内の処理はなるべくシンプルにしておきたかったので、上のような方法を採ってみました。他にもこんな工夫をしてるよ、みたいなのがあればコメント欄にて教えてください