🌟

PHPでスナップショットテストをやってみる

2024/08/24に公開

はじめに

PHPUnitとPestのそれぞれでスナップショットテストをやってみる。

環境

  • PHP 8.3.10

スナップショットテストとは

指定された期待値と保存した同じ値のスナップショットを比較してコードをテストする。
何もしていないのに壊れたなど予期していない変更を早期に発見する場合に便利な方法です。

やってみる

では、それぞれやってみます。

サンプルコード

今回のスナップショットテストで使うコードです。
適当なjsonを返すだけのコードです。

src/functions.php
<?php

declare(strict_types=1);

function json_render(): string
{
    $values = [
        [
            'name' => 'りんご',
            'price' => 150,
            "stock" => 10,
        ],
        [
            'name' => 'バナナ',
            'price' => 200,
            "stock" => 5,
        ],
        [
            'name' => 'みかん',
            'price' => 120,
            "stock" => 8,
        ],
    ];
    return json_encode($values);
}

PHPUnitでやってみる

PHPUnitは下記パッケージをインストールすることで実現できます。

https://github.com/spatie/phpunit-snapshot-assertions

PHPUnitと一緒にphpunit-snapshot-assertionsもインストールします。

composer require --dev phpunit/phpunit spatie/phpunit-snapshot-assertions

この記事を書いている時点では、それぞれ下記のバージョンがインストールされました。

phpunit/phpunit                    11.3.1
spatie/phpunit-snapshot-assertions 5.1.6

つぎにテストコードを作ります。
phpunit-snapshot-assertionsを使った場合は次のような感じになります。

大事なところに⭐️をつけてみました。

tests/SnapshotTest.php
<?php

declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots; // ⭐️

class SnapshotTest extends TestCase
{
    use MatchesSnapshots; // ⭐️⭐️

    #[Test]
    public function snapshot(): void
    {
        $this->assertMatchesJsonSnapshot(json_render()); // ⭐️⭐️⭐️
    }
}

スナップショットテストを行うためのtraitの読み込みを追加して、専用のアサーションメソッドを呼び出す。
assertMatchesJsonSnapshotメソッドはjson文字列に対してのスナップショットテストを行うためのメソッドです。

その他にも色々あるので、それはgithubのほうを見てください。

これだけでスナップショットテストを行うことができますので実行してみます。

vendor/bin/phpunit tests/SnapshotTest.php 

PHPUnit 11.3.1 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.10

I                                                                   1 / 1 (100%)

Time: 00:00.016, Memory: 8.00 MB

OK, but there were issues!
Tests: 1, Assertions: 3, Incomplete: 1.

初回実行ではまだスナップショットが作成されていないので、実行結果はI(未完了)になります。

実行するとSnapshotTest.phpと同じディレクトリに__snapshots__ディレクトリができています。
ここにスナップショットテスト用のファイルが作成され、今回だとSnapshotTest__snapshot__1.jsonファイルが作られています。

tree tests

tests
├── SnapshotTest.php
└── __snapshots__
    └── SnapshotTest__snapshot__1.json <- これが作成される

中身はこんな感じです。
なお、日本語の文字列はUnicodeエスケープされていますね。
変換されないようにするのはまた別の機会にやってみます。

tests/__snapshots__/SnapshotTest__snapshot__1.json
[
    {
        "name": "\u308a\u3093\u3054",
        "price": 150,
        "stock": 10
    },
    {
        "name": "\u30d0\u30ca\u30ca",
        "price": 200,
        "stock": 5
    },
    {
        "name": "\u307f\u304b\u3093",
        "price": 120,
        "stock": 8
    }
]

ファイルが作成された後にもう一回実行すると...

vendor/bin/phpunit tests/SnapshotTest.php

PHPUnit 11.3.1 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.10

.                                                                   1 / 1 (100%)

Time: 00:00.009, Memory: 8.00 MB

OK (1 test, 3 assertions)

成功しますね。2回目からはjsonファイルが存在するので、比較し差分がないのでテスト成功です。

つぎに、1回失敗させてみます。
サンプルコードの内容を適当にいじります。

        [
            'name' => 'りんご',
-           'price' => 150,
+           'price' => 200,
            "stock" => 10,
        ],

この状態で再度テストを実行すると...

vendor/bin/phpunit tests/SnapshotTest.php
PHPUnit 11.3.1 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.10

F                                                                   1 / 1 (100%)

Time: 00:00.017, Memory: 8.00 MB

There was 1 failure:

1) Tests\SnapshotTest::snapshot
Failed asserting that '[{"name":"\u308a\u3093\u3054","price":200,"stock":10},{"name":"\u30d0\u30ca\u30ca","price":200,"stock":5},{"name":"\u307f\u304b\u3093","price":120,"stock":8}]' matches JSON string "[{"name":"\u308a\u3093\u3054","price":150,"stock":10},{"name":"\u30d0\u30ca\u30ca","price":200,"stock":5},{"name":"\u307f\u304b\u3093","price":120,"stock":8}]".

Snapshots can be updated by passing `-d --update-snapshots` through PHPUnit's CLI arguments.
--- Expected
+++ Actual
@@ @@
 [
     {
         "name": "りんご",
-        "price": 150,
+        "price": 200,
         "stock": 10
     },

失敗しましたね、
これはスナップショットで用意したjsonファイルの内容と関数が返すjson文字列の内容異なるため失敗しました。
今回は意図的に変更していますが、もし何もしていないのにjson文字列の内容が変わったっていうことがこれで検知できるかと思います。

このあとは、不具合ならプログラムを直すことになりますが、今回はスナップショット側を変更します。
エラーメッセージ内にも書いてある-d --update-snapshotsを実行時のオプションで指定します。

vendor/bin/phpunit tests/SnapshotTest.php -d --update-snapshots 
PHPUnit 11.3.1 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.10

I                                                                   1 / 1 (100%)

Time: 00:00.017, Memory: 8.00 MB

OK, but there were issues!
Tests: 1, Assertions: 3, Incomplete: 1.

そうすると初回実行時と同じように結果はI(未完了)となり、スナップショットの再作成さて、再び実行すると成功しますね

vendor/bin/phpunit tests/SnapshotTest.php                      
PHPUnit 11.3.1 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.10

.                                                                   1 / 1 (100%)

Time: 00:00.008, Memory: 8.00 MB

OK (1 test, 3 assertions)

Pestでやってみる

Pestは元々機能が用意されているので、そちらを使います。

https://pestphp.com/docs/snapshot-testing

では、Pestのインストールから順番にやっていきます。

composer require pestphp/pest --dev --with-all-dependencies

この記事を書いている時点では、下記のバージョンがインストールされました。

pestphp/pest 2.35.1

では、つぎにテストコードを作っていきます。
PHPUnitに比べると書く量が少ないですね。

tests/SnapshotTest.php
<?php

declare(strict_types=1);

test('Snapshot-tests', function () {
    expect(json_render())->toMatchSnapshot();
});

Pestの場合はtoMatchSnapshotメソッドで色々対応しています。
expect関数に渡す配列でもokです。

では、こちらを実行してみます。

vendor/bin/pest tests/SnapshotTest.php 

   WARN  Tests\SnapshotTest
  … Snapshot-tests → Snapshot created at [tests/.pest/snapshots/SnapshotTest/Snapshot_tests.snap]                                                                                                            0.01s  

  Tests:    1 incomplete (0 assertions)
  Duration: 0.07s

PHPUnitと一緒で初回は結果がI(未完了)です。
実行時に表示していますが、Pestも初回はスナップショットファイルを作成が動きます。
phpunit-snapshot-assertionsとは異なり、スナップショットファイルはtests/.pest/snapshotsディレクトリ配下に作られます。

今回作られたファイルはこんな感じでした。

tree -a tests

tests
├── .pest
│   └── snapshots
│       └── SnapshotTest
│           └── Snapshot_tests.snap
└── SnapshotTest.php

ファイル名はテストケースから採用しているようです。

中身はこんな感じです。
PHPUnitの方と同様で日本語の文字列はUnicodeエスケープされていますね。

tests/.pest/snapshots/SnapshotTest/Snapshot_tests.snap
[{"name":"\u308a\u3093\u3054","price":150,"stock":10},{"name":"\u30d0\u30ca\u30ca","price":200,"stock":5},{"name":"\u307f\u304b\u3093","price":120,"stock":8}]

スナップショットファイルが作成されたので再度実行すると成功ですね。

vendor/bin/pest tests/SnapshotTest.php

   PASS  Tests\SnapshotTest
  ✓ Snapshot-tests                                                                                                                                                                                        0.01s  

  Tests:    1 passed (1 assertions)
  Duration: 0.07s

では、Pestの方でも1回失敗させてみます。
サンプルコードの内容を適当にいじります。

        [
            'name' => 'バナナ',
            'price' => 200,
-           'stock' => 5,
+           'stock' => 0,
        ],

この状態で再度テストを実行すると...

vendor/bin/pest tests/SnapshotTest.php

   FAIL  Tests\SnapshotTest
  ⨯ Snapshot-tests                                                                                                                                                                                        0.01s  
  ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  
   FAILED  Tests\SnapshotTest > Snapshot-tests                                                                                                                                                                   
  Failed asserting that the string value matches its snapshot (tests/.pest/snapshots/SnapshotTest/Snapshot_tests.snap).
Failed asserting that two strings are identical.
  -'[{"name":"\u308a\u3093\u3054","price":150,"stock":10},{"name":"\u30d0\u30ca\u30ca","price":200,"stock":5},{"name":"\u307f\u304b\u3093","price":120,"stock":8}]'
  +'[{"name":"\u308a\u3093\u3054","price":150,"stock":10},{"name":"\u30d0\u30ca\u30ca","price":200,"stock":0},{"name":"\u307f\u304b\u3093","price":120,"stock":8}]'

スナップショットの内容と異なる結果になったので失敗しましたね。
PHPUnitと同様、何もしていないのに失敗した場合、不具合ならプログラムを直すことになりますが、意図的に変更しているのでスナップショット側を変更します。

変更する場合は--update-snapshotsオプションをつけてテストを実行します。

vendor/bin/pest tests/SnapshotTest.php --update-snapshots

   WARN  Tests\SnapshotTest
  … Snapshot-tests → Snapshot created at [tests/.pest/snapshots/SnapshotTest/Snapshot_tests.snap]                                                                                                            0.01s  

  Tests:    1 incomplete (0 assertions)
  Duration: 0.06s

先ほどと一緒ですね。未完了で一回終わり、再度実行すると成功します。

vendor/bin/pest tests/SnapshotTest.php                   

   PASS  Tests\SnapshotTest
  ✓ Snapshot-tests                                                                                                                                                                                        0.01s  

  Tests:    1 passed (1 assertions)
  Duration: 0.06s

まとめ

今回はPHPのスナップショットテストをPHPUnitとPestで試してみました。
割と簡単に実施できることがわかりましたね。

PHPUnitとPestのどちらを採用するかは好みかもしれないですが、
実際にはAPIのレスポンスに対してスナップショットテストを使うともっと効果的に活用できると思います。

最後に今回使ったコードはこちらに置いてあります。

https://github.com/naopusyu/sandbox/tree/main/php/phpunit-snapshot-tests
https://github.com/naopusyu/sandbox/tree/main/php/pest-snapshot-tests

Discussion