今までの業務ではPHPのテストコードを PHPUnit で書いてきましたが、今年になってLaravel11からデフォルトのテストフレームワークとなった Pest でコードを書くことになりました。
PHPUnitからPestへの移行は内部でPHPUnitが動いていることもあり基本的にはスムーズですが、クラスベース から 関数ベース へのパラダイムシフトが起こり、特有の悩みポイントが発生したので、その対応について検討したことを書いてみたいと思います。
テストファイルに namespace を記述するかどうか
Pestの公式ドキュメントのサンプルコードや php artisan pest:test SampleTest コマンドで生成されるテストファイルには namespace が記述されていません。
またPestのテストコードは test() や it() といったグローバルに関数を呼び出して実装するので namespace を必ずしも記述する必要がありません。
対応
PHPにおける名前空間(namespace)はディレクトリ構造と一致させるのがデファクトスタンダードとなっており、直接自分で名前空間を記述してしまうと一致しなくなるリスクがあるので、namespace は記述しないほうが無難かと思います。
テストファイルに namespace を記述しない場合ですが、そのテストファイルで関数や定数を定義するとグローバルになるので、重複エラーを避けるため定義は避けたほうが良いと思います。
$this の型補完が効かない・静的解析(PHPStan等)でエラーになる
PHPUnitでは TestCase クラスの内部にコードを書くため、$this に対するIDE(PhpStormやVSCode)の型補完が標準で効きます。
しかし、Pestはクロージャ(関数)の中で $this を使うため、そのままではIDEやPHPStanが $this の正体を認識できずに画面が警告だらけになることがあります。
例えば beforeEach() 内で型付きプロパティのように扱おうとした時などです。
// Pestのクロージャ内
beforeEach(function () {
$this->actingAs($user); // IDEが「actingAsなんてメソッドはない」と警告する
}
対応
この悩みポイントに対しては以下の3つのアプローチがあると思います。
1. テストコードをPHPStanの対象から外す
PHPStanの主な目的を本番環境で動くアプリケーションコード(app/ や src/)のバグを防ぐことと捉えると、Pestが動く tests/ ディレクトリ全体をPHPStanの解析対象(paths)から除外するという割り切った運用が考えられます。
# phpstan.neon
parameters:
paths:
- app
- config
# - tests <-- あえて含めない
このようにしてしまえば、PestとPHPStanが衝突することは一切なくなりますが、$this の型補完は効かない問題は残ります。
2. Pest用のPHPStan拡張プラグインを導入する
テストコードも型安全に保ちたいという場合は、コミュニティが提供しているPHPStanの拡張プラグインを導入することで解決できます。
PestStan (MrPunyapal/PestStan) などのプラグインを導入すると、PHPStanがPest特有の構文やクロージャ内の $this の型を正しく理解できるようになります。
3. Laravel用ヘルパー関数を利用する
actingAs などの関数はPestが提供するLaravel用のヘルパー関数をインポートして直接呼び出す形にすればPHPStanのエラーを回避することが出来ます。
use function Pest\Laravel\actingAs;
use function Pest\Laravel\get;
it('can access admin dashboard', function () {
$user = User::factory()->create();
// $this-> を使わないので、PHPStanが「$thisの型」に迷わなくなる
actingAs($user)
->get('/admin')
->assertStatus(200);
});
ただしサードパーティ製のパッケージが提供する独自のテスト用トレイトなどを利用する場合、どうしても $this->someCustomMethod() と書かざるを得ないシーンが出てきた時に課題は残ります。
所感
どのアプローチを取るかはケースによると思いますが、Pest独自の書き方をするたびにPHPStanのエラーが起きやすく感じたので、開発体験を優先するならテストコードをPHPStanの対象外とするのが良さそうに思います。
もしテストコードも型安全に保ちつつ、極力プラグインやサードパーティ製パッケージの依存も減らしたい場合、Laravel用のヘルパー関数を利用する方法も選択肢としてありかなと感じました。
テストクラス内にあった「プライベートメソッド」の行き場に困る
PHPUnitでは、そのテストクラス内だけで使い回すちょっとしたヘルパー処理を private function createDummyUser() のように定義できました。しかし、Pestはクラスではないため、そのままプライベートメソッドを書く場所がありません。
対応
この悩みポイントに対して以下の3つのアプローチがあると思います。
1. ヘルパー関数化
テスト全体で使用する関数は tests/Helpers.php にグローバル関数として定義し、テストケース毎など狭い範囲で使用する関数は tests/Helpers/ 以下に namespace 付きでファイルを作成してヘルパー関数を定義します。
2. Trait(トレイト)化
共通処理をTraitにまとめ、tests/Pest.php の uses(MyTrait::class)->in('Feature') でインポートしたり、適用したいテストファイルに対してインポートします。
3. 変数のクロージャ化
同じファイル内であれば、テストケースの外側で $createDummyUser = function() { ... }; のように変数として関数を定義し、テスト内で呼び出します。
$createDummyUser = fn () => User::factory()->create();
it('can access admin dashboard', function () use ($createDummyUser) {
actingAs($createDummyUser)
->get('/admin')
->assertStatus(200);
});
所感
ヘルパー関数化とTrait(トレイト)化は競合している部分があるので、両方用いるよりは片方に統一させたほうが無秩序となることを防ぎ、保守がしやすくなると思います。
さらに変数のクロージャ化と併用することで、やりたいことは概ねカバーできるのではと思います。
ちなみにPest公式ドキュメントでは、
If you're transitioning to a functional approach for writing tests, you may wonder where to put your helpers that used to be protected or private methods in your test classes. When using Pest, these helper methods should be converted to simple functions.
引用元:https://pestphp.com/docs/custom-helpers
と記述がありヘルパーに実装することを勧めている様に感じました。
最後に
この記事が私と同じようなことで悩んでいる方の一助になれば幸いです。
悩みポイントへの対応はPestの設計思想になるべく沿わせているので、融通の利いていないものに見えるかもしれません。
この記事に書いてあることが正解ではありませんので、実際に対応する際は参考程度に見て頂き、各自の設計思想や状況に合わせて工夫して頂ければと思います。