例外をテストする、expectExceptionの正しい置き場所
例外のテストとは、expectException で「これから例外が起きるはず」と宣言してから、例外を起こすコードを呼んで検証する書き方です。宣言は必ず発生前に置くのがポイントです。
expectExceptionは「発生前」に宣言する
異常系のテストで意外とハマるのが、例外の検証の書き方です。expectException は「これから例外が起きるはず」という宣言なので、例外を起こすコードを呼ぶ前に書く必要があります。順番を間違えると、宣言が実行される前に例外が飛んでテストが期待どおり動きません。宣言してから実行、という並びが基本ですね。
public function test_不正なメールで例外(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('これはメールではない'); // 宣言のあとに呼ぶ
}
メッセージやコードも確かめられる
クラスだけでなく、例外メッセージやコードも検証できます。expectExceptionMessage は部分一致なので、全文を書かなくてもキーワードが含まれていれば通ります。ただしメッセージに寄せすぎると文言変更で壊れやすくなるので、私はクラスの検証を主にして、メッセージは要点だけ見ることが多いですね。
$this->expectException(DomainException::class);
$this->expectExceptionMessage('残高が不足'); // 部分一致でOK
$wallet->withdraw(10000);
「例外が起きなかった」も落としたい
expectException を書いておけば、例外が飛ばなかった場合はテストが失敗してくれます。ただ、一つのテストに例外を起こす行の後ろに別のアサーションを続けると、そこには到達しないので注意です。例外の後で何かを確かめたいなら、try/catch で捕まえて状態を検証するスタイルにします。
try {
$wallet->withdraw(10000);
$this->fail('例外が飛ぶはず'); // ここに来たら失敗
} catch (DomainException $e) {
$this->assertSame(5000, $wallet->balance()); // 状態は変わっていない
}
まとめ
基本は expectException を先頭近くに宣言して、例外を起こす一行を最後に置く、という形で十分だと思います。メッセージ検証は便利ですが依存しすぎないように、というのが個人的な塩梅ですね。例外が起きた「あとの状態」まで見たいときだけ try/catch と fail を組み合わせる、と切り替えると異常系のテストがすっきり書ける気がします。
よくある質問
Q. expectException はどこに書けばいいですか?
A. 例外を起こすコードを呼ぶ前に宣言します。順番を逆にすると宣言前に例外が飛んでしまい、期待どおり検証できません。先頭近くに置くのが基本です。
Q. 例外が起きたあとの状態も確かめたいときは?
A. expectException の後ろに書いたアサーションには到達しないので、try/catch で例外を捕まえ、catch の中で状態を検証するスタイルに切り替えます。