テストを書くとき、テストパターンや入力値の組み合わせが増え、データの管理が煩雑になりがちです。Pestではwith()dataset()を使って基本的なデータ整理ができますが、後から読んでも理解しやすい・修正しやすいコードを目指して、Combining DatasetsSharing Datasetsを活用し、テストコードを整理してみます。

テスト対象

テスト対象は以下のクラスです。calculateShippingCost()は、引数として渡された温度帯配送地域の値に応じて送料を計算する関数です。

class ShippingCalculator
{
    private static array $tempCosts = [
        '常温' => 0,
        '冷蔵' => 200,
        '冷凍' => 500,
    ];

    private static array $regionCosts = [
        '東京' => 500,
        '大阪' => 750,
        '福岡' => 1000,
    ];

    /**
     * 温度帯と配送地域から送料を計算する
     */
    public static function calculateShippingCost(string $temp, string $region): int|string
    {
        if (!array_key_exists($temp, self::$tempCosts) || !array_key_exists($region, self::$regionCosts)) {
            return '計算不可';
        }
    
        $baseCost = self::$regionCosts[$region];

        $tempCost = self::$tempCosts[$temp];
        
        return $baseCost + $tempCost;
    }
}

例えば、温度帯が「冷蔵」の場合は200円、配送地域が「大阪」の場合は750円を取得し、合計送料として950円を返します。

> ShippingCalculator::calculateShippingCost('冷蔵', '大阪');
= 950

また、定義されていない値が渡された場合は「計算不可」が返ります。

> ShippingCalculator::calculateShippingCost('でたらめ', '大阪');
= "計算不可"

テストコード

以下はcalculateShippingCost()の計算結果が正しいことを検証するテストコードです。

テストのパターンとして、温度帯と配送地域をいくつかピックアップしてテストする形でもいいかもしれませんが、今回は全ての組み合わせ(3つの温度帯×3つの配送地域 = 9パターン)をテストします。

it('全ての組み合わせパターンで送料を検証', function (string $temp, int $tempCost, string $region, int $regionCost) {

    // 期待される合計送料
    $expected = $tempCost + $regionCost;
        
    // 実際の計算結果
    $actual = ShippingCalculator::calculateShippingCost($temp, $region);
    
    expect($actual)->toBe($expected);
})
->with([
    ['常温', 0, '東京', 500],    // 1回目
    ['常温', 0, '大阪', 750],    // 2回目
    ['常温', 0, '福岡', 1000],   // 3回目
    ['冷蔵', 200, '東京', 500],  // 4回目
    ['冷蔵', 200, '大阪', 750],  // 5回目
    ['冷蔵', 200, '福岡', 1000], // 6回目
    ['冷凍', 500, '東京', 500],  // 7回目
    ['冷凍', 500, '大阪', 750],  // 8回目
    ['冷凍', 500, '福岡', 1000], // 9回目
]);

Pestでは、with()を使用してデータパターンをテストに渡します。with()には複数のテストデータを配列で渡せるため、今回のように多次元配列を使うことで、各配列が1回分のテストデータとして処理されます。

まずはwith()に9パターンを直接記述しましたが、パターンが多いとテストコードと繋がって少しごちゃごちゃして見えますね。そういう場合はdataset()を使用すると、以下のようにテストとパターンを分離させて書くことができます。

it('全ての組み合わせパターンで送料を検証', function (string $temp, int $tempCost, string $region, int $regionCost) {
・・・テストコードは同じ
})
->with('shippingCosts');

dataset('shippingCosts', [
    ['常温', 0, '東京', 500],
    ['常温', 0, '大阪', 750],
    ['常温', 0, '福岡', 1000],
    ['冷蔵', 200, '東京', 500],
    ['冷蔵', 200, '大阪', 750],
    ['冷蔵', 200, '福岡', 1000],
    ['冷凍', 500, '東京', 500],
    ['冷凍', 500, '大阪', 750],
    ['冷凍', 500, '福岡', 1000],
]);

dataset()は第一引数にデータセット名を、第二引数は先ほどwith()に渡していたデータの配列を指定します。

そしてwith()の方へはデータセット名を指定するだけで良いのでテストが少しすっきりと、可読性が良くなりました。

Combining Datasetsの活用

ここまでの書き方でも問題ありませんが、9パターンが並んでいて分かりにくい点や、パターンが増減した際の修正が大変な点が気になります。

そこで、Combining Datasetsを活用します。全ての組み合わせをテストする時はCombining Datasetsを使うとコードをとても簡潔に書くことができます。

まずは9パターンあったデータセットを、「温度帯」と「配送地域」のそれぞれのデータセットに分けて作成します。

dataset('tempWithCost', [
    ['常温', 0],
    ['冷蔵', 200],
    ['冷凍', 500],
]);

dataset('regionWithCost', [
    ['東京', 500],
    ['大阪', 750],
    ['福岡', 1000],
]);

「温度帯」の3パターンはtempWithCostに、「配送地域」の3パターンはregionWithCostと定義しました。

そしてこれをテスト側でどう呼び出すかというと、以下のようにwith()を連結させるだけです。

it('全ての組み合わせパターンで送料を検証', function (string $temp, int $tempCost, string $region, int $regionCost) {
...
})
->with('tempWithCost')
->with('regionWithCost');

ちゃんと9パターン全て網羅されるのか、テストを実行して確認します。

PASS  Tests\Unit\ShippingCalculatorTest
  ✓ it 全ての組み合わせパターンで送料を検証 with ('常温', 0) / ('東京', 500)                                                                                                0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('常温', 0) / ('大阪', 750)                                                                                                0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('常温', 0) / ('福岡', 1000)                                                                                               0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷蔵', 200) / ('東京', 500)                                                                                              0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷蔵', 200) / ('大阪', 750)                                                                                              0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷蔵', 200) / ('福岡', 1000)                                                                                             0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷凍', 500) / ('東京', 500)                                                                                              0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷凍', 500) / ('大阪', 750)                                                                                              0.01s  
  ✓ it 全ての組み合わせパターンで送料を検証 with ('冷凍', 500) / ('福岡', 1000)                                                                                             0.01s  

問題なさそうです。出力にはテスト名だけでなく、どのパターンの組み合わせかも表示してくれています。ちなみにテスト失敗時は以下のようになり、どのケースで失敗したかも分かりやすいです。

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 FAILED  Tests\Unit\ShippingCalculatorTest > it 全ての組み合わせパターンで送料を検証 with ('冷凍', 500) / ('九州', 1000)                                                         
  Failed asserting that '計算不可' is identical to 1500.

データセットの再利用

dataset()でデータセットとテストを分離しておくと、他のテストでも再利用することができます。

例としてもう1つテストを作成します。今度は、定義されていない温度帯の場合は”計算不可”が返るパターンのテストです。

it('存在しない温度帯は計算不可を検証', function (string $temp, string $region) {

    $actual = ShippingCalculator::calculateShippingCost($temp, $region);
    
    expect($actual)->toBe('計算不可');
})
->with(['cool', ' ', '存在しない温度帯'])
->with('regionWithCost');

こちらもCombining Datasetsを使用しており、1つめのwith()には失敗パターンの温度帯データを直接記述しています。

そして2つ目のwith()には、すでに定義済みのregionWithCost(配送地域のデータセット)を渡しました。

このように、テストコード内でパターンを重複することなく複数のテストで共有できます。テストを実行してみましょう。

   PASS  Tests\Unit\ShippingCalculatorTest
  ✓ it 存在しない温度帯は計算不可を検証 with ('cool') / ('東京', 500)                                                                                                   0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with ('cool') / ('大阪', 750)                                                                                                   0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with ('cool') / ('福岡', 1000)                                                                                                  0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with (' ') / ('東京', 500)                                                                                                      0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with (' ') / ('大阪', 750)                                                                                                      0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with (' ') / ('福岡', 1000)                                                                                                     0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with ('存在しない温度帯') / ('東京', 500)                                                                                               0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with ('存在しない温度帯') / ('大阪', 750)                                                                                               0.01s  
  ✓ it 存在しない温度帯は計算不可を検証 with ('存在しない温度帯') / ('福岡', 1000)                                                                                              0.01s  

こちらも問題なく全ての組み合わせでテストできました!

datasetをファイルへ切り出す

ここまでは、テストコードとデータセットが1つのファイルに共存している状態でした。Pestではデータセットをtests/Datasetsへ切り出すことで、他のテストファイルからも呼び出せるようになるSharing Datasetsという機能があります。

データセットをファイルに切り出す際、Laravel環境では前回ご紹介したプラグインををインストールしていると以下のコマンドで簡単にファイルを作成できます。もちろん手動で作成しても問題ありません。

$ php artisan pest:dataset shippingCosts

ファイル名はshippingCostsとしました。ここに、先ほどのデータセットを移行します。テストファイルに記述していたdataset()をそのままコピーすればOKです。

dataset('tempWithCost', [
    ['常温', 0],
    ['冷蔵', 200],
    ['冷凍', 500],
]);

dataset('regionWithCost', [
    ['東京', 500],
    ['大阪', 750],
    ['福岡', 1000],
]);

これで、テストコードとデータセットを完全に分離できました!

最初のテストコードよりはかなりすっきりしたのではないでしょうか。Pestでテストを作成する際は、ぜひ活用してみてください。

メルマガ購読の申し込みはこちらから。

By hmatsu