Pest V2で追加された新機能10選
Pestとは
PHPのTesting Frameworkの一種
一番有名なのは、PHPUnitで、2番目にPestらしい
Pestは、シンプルさを追求している。
JavaScript Testing FrameworkのJestと名前が酷似しているが、書き方も非常に似ている
Jest慣れている人は、PHPUnitより断然Pestの方が書きやすいと思う
Pest V2
2023/3/20にPestのバージョン2がリリースされた。
PestのWebサイトデザインも丸々変更するほどの気合の入りっぷり。
PestV2の開発には、約18ヶ月の歳月をかけて、500コミット以上しているとのこと。
主要な変更点
1. Architecture Testing
- 全体のコードの品質維持
2. Optimizing Tests / Parallel Testing
- 並列テスト時の実行速度向上
3. Optimizing Tests / Profiling
- テスト実行結果の可視化
4. Optimizing Tests / Compact Printer
- テスト実行後、失敗したテストのみ表示
5. Test Filtering / --retry
- 前回失敗したテストのみ実行
6. Test Filtering / --dirty
- 変更があったファイルのみテストを実行
7. Test Filtering / --bail
- テストに失敗した時点で、テスト実行を停止
8. Skipping Tests / Creating Todos
- テストスーツの中でTodo管理可能
9. Custom Expectations / Intercept Expectations
- 各expectaions関数の動作を上書きすることが可能
10. Datasets / Scoped Datasets
- 特定の機能やとあるファイル内でのみ使用するDatasetを定義可能
各変更点の詳細は、明日以降記載していく。
1. Architecture Testing
- 全体のコードの品質維持
アプケーションは、時間が経てばコードはレガシーになっていき、開発する人も変わっていくものなので全体のコードの品質維持がとても難しくなる。
そういった悩みを解決するのが、Architecutre Testingである。
これを使って、アーキテクチャルールを明確にして、テストを記述しておけば、プルリクエストでレビューする前に何が間違っているかを見つけることができる。
無駄なレビュー工数の削減により、より速いデプロイが可能になり、チームの生産性向上にもつながる。
できること
toOnlyUse()
-
あるクラス内で、特定の名前空間を持つクラスや関数のみ呼び出されているかをテスト
- モデルクラス内では「Illuminate\Database」の名前空間を持つクラスが呼び出されているかどうかをテスト
test('models') ->expect('App\Models') ->toOnlyUse('Illuminate\Database');
toOnlyBeUsedIn()
-
特定のクラスが、アプリケーションの特定の一部でしか使用されないことをテスト
- モデルクラスが、リポジトリクラスでのみ使用されていることをテスト
test('models') ->expect('App\Models') ->toOnlyBeUsedIn('App\Repositories');
toBeUsed()
-
あるクラスや関数がアプリケーション全体で呼び出されていないかをテスト
- dd() dump()などが残っていないことをテスト
test('globals') ->expect(['dd', 'dump']) ->not->toBeUsed();
toBeUsedIn()
-
あるクラス内で、特定の関数やクラスが使用されていないことをテスト
- ドメインクラス内で「Illuminate\Http」の名前空間を持つクラスが使用されていないことをテスト
test('globals') ->expect('Illuminate\Http') ->not->toBeUsedIn('App\Domain');
toUse()
-
特定の関数やクラスが、あるクラス内で使用されていないことをテスト
- 「request」関数が、ドメインクラス内で使用されていないことをテスト
test('globals') ->expect('App\Domain') ->not->toUse('Illuminate\Http');
toUseNothing()
-
特定の名前空間やクラス内で、依存関係が1つもないことをテスト
- 「App\ValueObject」の名前空間を持つクラス内で、依存関係がないことをテスト
test('value objects') ->expect('App\ValueObjects') ->toUseNothing();
ignoring()
- 既出のメソッドと組み合わせて使用することで、特定の名前空間やクラスを例外扱いにできる
- 「Illuminate\Support\Facade」の利用を制限したいが、プロバイダークラスは例外として利用を許可する
test('facades')
->expect('Illuminate\Support\Facades')
->not->toBeUsed()
->ignoring('App\Providers');
2. Optimizing Tests / Parallel Testing
- 並列テスト時の実行速度向上
Pestはデフォルトだと、単一のプロセス内でテストを実行する。
しかし、オプション --parallel
を付与することで、マルチプロセスで並列テストを実行することが可能
その並列テストの実行速度をV1と比較すると、Pest V2では60%ほど向上している。
V1からV2にかけて、並行テストのコアシステムをリビルドしたおかげでこれほどまでの速度向上が得られたとのこと。
並列テスト実行
./vendor/bin/pest --parallel
並列テスト実行(プロセス数指定)
./vendor/bin/pest --parallel --processes=10
3. Optimizing Tests / Profiling
- テスト実行結果の可視化
テストをたくさん書くと、テスト全体の実行速度がどんどん長くなります。
一般的には、テスト終了後は、全体の実行時間しか出力されずどのテストケースがボトルネックになっているかを見つけるのは手作業で1つずつテストを走らせて確認するという気が遠くなる作業が必要です。
しかし、--profile
のオプションを付けることで、テスト実行後に実行時間が長いテストケースを一覧表示してくれるので、修正すべきテストケースが明確になります。
テスト実行(--profileあり)
./vendor/bin/pest --profile
4. Optimizing Tests / Compact Printer
- テスト実行後、失敗したテストのみ表示
テストを実行すると、通常は成功したテストと失敗したテストが全て表示されます。
しかし、大量のテストを書いていると、大事なのはどこでテストが失敗しているかです。
そこで、--compact
のオプションを付けることで、失敗したテストのみテスト結果に出力します。
これにより、修正すべきテストが明確になり、生産性向上につながります。
テスト実行(--compactあり)
./vendor/bin/pest --compact
5. Test Filtering / --retry
- 前回失敗したテストのみ実行
失敗したテストの修正を確認する際に、毎回全テストを動かすのは非効率です。
失敗したテストが1つや2つなら、テストクラスを指定して動かすのでも良いですが、失敗したテストが多くのテストクラスに跨っている場合、当てはまるテストクラスを全て指定するのは非常にめんどくさいです。
そんな時に、--retry
のオプションを使うと、前回失敗したテストのみを抽出して再実行してくれます。
テスト実行(--retryあり)
./vendor/bin/pest --retry
6. Test Filtering / --dirty
- 変更があったファイルのみテストを実行
新機能の開発を行なっている場合、それ以前に書いたテストへは一切影響がないことがほとんどです。
そんな時に、--dirty
オプションを使うと、Gitの変更履歴を参照して、今回変更があったファイルと関連するテストのみ実行することが可能です。
テスト実行(--dirtyあり)
./vendor/bin/pest --dirty
- Test Filtering / --bail
- テストに失敗した時点で、テスト実行を停止
通常だと、テストが失敗してもしなくても全てのテストが実行されます。
しかし、--bail
オプションを利用することで、テストが1つ失敗した時点で、テスト実行が終了します。
失敗するテストケースがあまりに多いとやる気を無くしてしまいがちなので、このオプションを使って1つずつ失敗するテストケースを修正できるのは良さそう。
テスト実行(--bailあり)
./vendor/bin/pest --bail
8. Skipping Tests / Creating Todos
- テストスーツの中でTodo管理可能
テストを書かないといけないが、書く時間がない時は、意外とよくあるものです。
しかし、人間は忘れる生き物なので、何か痕跡を残しておかないと、テストを書かないままずっと放置してしまうこともよくあります。
そこで、todo()
を使用することで、テストを書くべきという痕跡を簡単に残すことができます。
そして、現在のtodo一覧が見たい時も、--todo
オプションを利用することで簡単に確認することができます。
todo管理
it('database test')->todo();
test('page transition test')->todo();
todo一覧表示
./vendor/bin/pest --todo
9. Custom Expectations / Intercept Expectations
- 各expectaions関数の動作を上書きすることが可能
通常は、デフォルトで用意されているexpectations関数を用いれば、大抵のテストケースは問題なく実行可能です。
もし既存のexpectations関数の動作を上書きしたい場合は、intercept関数
を使いましょう。
toBe()
例:本来、toBe関数は、expect()内に記載された値がtoBe()内の値と一致しているかのアサーションを実行します。
expect(1)->toBe(1); // 1 = 1 であることをアサーション
このtoBe関数を、ModelクラスのIDが一致しているかどうかというアサーションに変更したい場合は、intercept関数
を用いて以下のように書きます。
// tests/Pest.php or tests/Expectations.php
expect()->intercept('toBe', Model::class, function(Model $expected) {
expect($this->value->id)->toBe($expected->id);
});
従来通りのtoBe
関数であれば、以下のように書きます。
// tests/Feature/ExampleTest.php
test('models', function () {
$userA = User::find(1);
$userB = User::find(1);
// IDを明示的に取得する必要がある。
expect($userA->id)->toBe($userB->id);
});
しかし、今回の場合intercept関数で、toBe関数内部でidを取り出すようにしているので、以下のような書き方で同じアサーションが可能です。
// tests/Feature/ExampleTest.php
test('models', function () {
$userA = User::find(1);
$userB = User::find(1);
// モデルクラスを渡すだけ(idの値はtoBeの内部で検証される)
expect($userA)->toBe($userB);
});
10. Datasets / Scoped Datasets
- 特定の機能やとあるファイル内でのみ使用するDatasetを定義可能
Pestでは、データセットをどこでも作成することが可能です。
dataset('products', [
'egg',
'milk'
]);
しかし、時には、データセットが特定の機能やフォルダーセットに依存する場合があります。
このような場合、Datasetsフォルダ全体にデータセットを配布するのではなく、必要なデータセットを要求する関連するフォルダ内にDatasets.phpファイルを生成し、データセットの範囲をそのフォルダに制限することができます。
先ほどのproducts
のデータセットを例に挙げてみます。
tests/Feature/Products/Datasets.phpにproducts
データセットを作成した場合
// 使用可能
// tests/Feature/Products/ExampleTest.php...
it('has products', function (string $product) {
expect($product)->not->toBeEmpty();
})->with('products');
// 使用不可
// tests/Feature/Users/ExampleTest.php...
it('has users', function (string $product) {
expect($product)->not->toBeEmpty();
})->with('products');
このように、データセットが使用できる範囲を簡単に制限することができます。
前者のテストは、products
データセットが置かれているディレクトリ内に書かれたテストなので、products
データセットを利用することができます。
反対に、後者のテストはproducts
データセットが置かれているディレクトリ外で書かれたテストなので、products
データセットを利用することができません。