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

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

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

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


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

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

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

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

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

<img width="672" alt="スクリーンショット 2018-12-01 20.03.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/297030/8eb52c7e-e5ee-c5c7-3b00-38f5ffb5ef22.png">

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

が、
**検証環境でメールが届いてない、だと･･･？**


###なぜ起こったのか

単純明快！
**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にするよう修正。

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

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

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

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

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

###なにが起こったのか

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

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

<img width="672" alt="スクリーンショット 2018-12-01 20.03.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/297030/8eb52c7e-e5ee-c5c7-3b00-38f5ffb5ef22.png">

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

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

###なぜ起こったのか
**ユーザーが削除された場合を考えていなかった。**

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

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

```php
$item->user->email;
```

期限間近のアイテムを取得
↓
そのアイテムの持ち主を取得
↓
持ち主のメールアドレスを取得


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

###対処法

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

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

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

```php
$test_user->delete();
```

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

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

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

※`$no_send_user1`と`$no_send_user2`は、どちらも｢送られてはいけない｣条件のユーザー

```php
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);
});
```

はい、
<img width="672" alt="スクリーンショット 2018-12-01 20.03.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/297030/8eb52c7e-e5ee-c5c7-3b00-38f5ffb5ef22.png">

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

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

###なぜ起こったのか
**`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で繋げることです。

```php
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.最後に
自分の想定範囲が甘いと、
せっかくのテストが意味を成さなくなってしまいます。

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

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

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