【Laravel/PHPUnit】テスト通ったのにエラー!初心者にありがちな「テストケースの想定漏れ」実例3選


tadoumaです。
早いものでエンジニアになって5ヶ月、
最近、テストが通ったのに検証でエラーや不具合が起こることが増えました。
今回は、初心者に起こりがちな「テストの想定漏れ」について書いてみようと思います!

目次

1.はじめに 想定漏れについて
2.想定漏れパターン3選
3.最後に

1.はじめに 想定漏れについて

なぜテストが通っても検証環境でエラーになるのか。
それは「自分が書いたテストの想定範囲が足りなかった」からです。
特に初心者はまだ蓄積しているパターンが少ないため、想定が甘いことが多々あります。(ありました。。。)
今回の記事では、自分が実際にやらかした「想定漏れ」について紹介していきます。

2.想定漏れパターン3選

2-1.Carbonでの時間指定でハマった

なにが起こったのか

登録日から3日経過したユーザーに対し、リマインドメールを送信するバッチを作成した時、
それは起こりました。

登録日から3日前のユーザーを擬似的に作ります。
3日前はもちろん便利なCarbonで作成。

$test_user = factory(User::class)->create(['created_at' => Carbon::today()->subDays(3)]);

テスト通った〜〜〜〜!(ΦωΦ)

が、
検証環境でメールが届いてない、だと・・・?

なぜ起こったのか

単純明快!
0:00以外の時間を想定していなかった
そのため、日付のカウントが1日ずれてしまっていました。

例)
今日の日付を12/15 0:00とした時、、、
A︰テストデータ(12/12 0:00)の場合
12/15  0:00と比較した時、その差は3日
B︰DBに実際にある日付(12/12 0:01)の場合
12/15 0:00と比較した時、その差は2日と23時間59分

そのため、パターンBの場合はカウントした日付が2日になってしまい、送られないのです。

今回、テストデータはCarbonでの作成のため、
全て0:00の日付で作成されていて気づかなかったという・・・。

対処法

実コードは、取得した日付について、時間を0:00にするよう修正。

$created_at = Carbon::parse($user->created_at)->startOfDay();

テストは、0:00以外の時間を設定するよう変更。

Carbon::setTestNow(Carbon::create(2018, 12, 15, 0, 0, 1));

これで無事、日付のカウントが動くようになりました。
この日付の感覚、初心者だと最初はなかなか掴みづらい気がします。

2-2.null,0,削除パターン

なにが起こったのか

まず前提はこちら。
・メール送信バッチ
・ユーザーはアイテムを保持する
・アイテムには期限がある
・ユーザーは削除可能である
・アイテムの期限が切れそうなユーザーに「アイテムを使ってください」とメールを送る

テストでは、
・アイテムを保持するユーザー
・アイテムを保持しないユーザー
この2つを作り、それぞれ送られる/送られないをテストしました。

テスト通った〜〜〜〜!ヾ(ΦωΦ)/

と、思いきや。。。。。
またもや検証環境でメールが届いてない・・・?
エラーメッセージ、Trying to get property of non-object
が出てしまいました・・・

なぜ起こったのか

ユーザーが削除された場合を考えていなかった。

お察しの通りです。
(ユーザーが削除されたらアイテムも消そうよ、というツッコミは一旦置いておいて。。。)

今回、こんな感じでメールアドレスを取得しようとしていました。

$item->user->email;

期限間近のアイテムを取得

そのアイテムの持ち主を取得

持ち主のメールアドレスを取得

ま〜当然ユーザーいなかったら、nullからemailを取ろうとしているので無理ですね!
すみませんでした!!!

対処法

実コードは、$userがなかったときの処理を追加。

if(empty($user)){
    return false;
}

テストでは、削除されたユーザーを入れておく。

$test_user->delete();

これで完了!
私は「nullだったら」「削除されたら」「0だったら」といった想定を忘れて
検証環境でエラーになることが多かったです。。。

2-3.否定アサーションと接続詞

なにが起こったのか

送られてはいけない条件のユーザーに、メールが送られていないことをテストしていた時、、、

$no_send_user1$no_send_user2は、どちらも「送られてはいけない」条件のユーザー

Mail::assertNotQueued(TestMail::class, function($mail) use ($no_send_user1, $no_send_user2){
    return $mail->hasTo($no_send_user1->email) and
    $mail->hasTo($no_send_user2->email);
});

はい、

テスト通った〜〜〜〜!ฅ(* ΦωΦ *) ฅ

と、思った時期が私にもありました。
しかし実際にはメールが送られてしまっていたのです・・・・
原因は単純だったので置いておいて、「なぜこれが検知できなかったか」で考えてみます。

なぜ起こったのか

not A and B と、not A or Bを誤認していた。
あ、こいつ文系だなって感じですね。
ただ私これ、専攻の英語でも勉強してた・・・・・。

平たく言うと、
not A and B→部分否定
(Aでない または Bでない)
not A or B→全否定
(Aでない かつ Bでない)
これを取り違えていたのです。

上記のコードの場合、
$no_send_user1$no_send_user2
どちらか一方に送られていなければ、それでTrueを返していた、ということ。

つまり、$user1$user2 どちらにも送られていないことを確認したい場合、
andではなくorで繋げなければいけなかったのです。。

対処法

上記で言っちゃいましたが、orで繋げることです。

Mail::assertNotQueued(TestMail::class, function($mail) use ($no_send_user1, $no_send_user2){
    return $mail->hasTo($no_send_user1->email) or
    $mail->hasTo($no_send_user2->email);
});

知らないと一度はやりそうなミス。。

3.最後に

自分の想定範囲が甘いと、
せっかくのテストが意味を成さなくなってしまいます。

では、どうすれば想定漏れがなくなるのか?
【初心者が気をつけること】としては、
・他の人のテストコードを読み込む。必ず「ここまで想定する?ここまで場合分けする!?」って思うことがあるはず。
・テストケースを作成する際、場合分けした後に「もし○○だったら」を思いつく限り書いてみる
(今回だと、「もしユーザーが削除されていたら」を考えつけていればエラーは起こらなかった)

簡単ですが、このあたりが対処法として考えられるかと思います。
私も今後「もし○○だったら」を考えられるよう、日々精進していこうと思いました。

今回の記事が少しでもなにかの参考になれば幸いです!