PHPでスナップショットテストをやってみる
はじめに
PHPUnitとPestのそれぞれでスナップショットテストをやってみる。
環境
- PHP 8.3.10
スナップショットテストとは
指定された期待値と保存した同じ値のスナップショットを比較してコードをテストする。
何もしていないのに壊れたなど予期していない変更を早期に発見する場合に便利な方法です。
やってみる
では、それぞれやってみます。
サンプルコード
今回のスナップショットテストで使うコードです。
適当なjsonを返すだけのコードです。
<?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は下記パッケージをインストールすることで実現できます。
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を使った場合は次のような感じになります。
大事なところに⭐️をつけてみました。
<?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エスケープされていますね。
変換されないようにするのはまた別の機会にやってみます。
[
{
"name": "\u308a\u3093\u3054",
"price": 150,
"stock": 10
},
{
"name": "\u30d0\u30ca\u30ca",
"price": 200,
"stock": 5
},
{
"name": "\u307f\u304b\u3093",
"price": 120,
"stock": 8
}
]
ファイルが作成された後にもう1回実行すると。。。
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は元々機能が用意されているので、そちらを使います。
では、Pestのインストールから順番にやっていきます。
composer require pestphp/pest --dev --with-all-dependencies
この記事を書いている時点では、下記のバージョンがインストールされました。
pestphp/pest 2.35.1
では、つぎにテストコードを作っていきます。
PHPUnitに比べると書く量が少ないですね。
<?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エスケープされていますね。
[{"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
先ほどと一緒ですね。未完了で1回終わり、再度実行すると成功します。
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のレスポンスに対してスナップショットテストを使うともっと効果的に活用できると思います。
最後に今回使ったコードはこちらに置いてあります。
Discussion