Laravel8で開発をしています。
とある関数があります。チケットの有効期限切れをチェックする関数です。
ticketsテーブルには start_date
と end_date
があります。date型です。
Ticketモデルで、dates を設定して、 start_date と end_date は Carbonでキャストされています。
有効期限切れの関数をこのように書きました。
/**
* 日付を変形する属性
*
* @var array
*/
protected $dates = [
'start_date',
'end_date',
];
/**
* expired
* チケットが今日有効期限切れか?
*
* @param int $ticket_id
* @return bool 有効期限切れ true 有効期限は切れていない false
*/
public static function expired(int $ticket_id): bool
{
$ticket = self::find($ticket_id);
// チケット が null の場合 有効期限切れとする
if (empty($ticket)) {
return true;
}
$today = Carbon::today();
if ($today->lt($ticket->start_date) || $today->gt($ticket->end_date)) {
return true;
} else {
return false;
}
}
Ticketのidを渡して、そのデータを取得します。
もしnullだったら、有効期限切れ扱いにします。return true
今日の日付をとってきて、開始日未満、もしくは終了日より大きければ有効期限切れです。return true
それ以外は return false
で、有効期限内となります。
一見、普通の処理ですが、これだと非常にUnitTestがやりにくいです。特に、「今日」というデータを取得するテストは非常にやりにくいです。なぜならば、UnitTestはこの先ずっと、開発が続く限り実行されていきます。テストを作成したときは有効だったものも、例えば、1カ月後とか来年とかはどうでしょうか?有効なデータを作るには、今有効なデータをデータベースに登録しておかなければいけません。
こんなとき、私はこの関数の引数に日付を渡すように変更してしまいます。
こうです。
/**
* expired
* チケットが第2引数の日に有効期限切れか?
*
* @param int $ticket_id
* @param Carbon $today null の場合は、今日になる
* @return bool 有効期限切れ true 有効期限は切れていない false
*/
public static function expired(int $ticket_id, Carbon $today = null): bool
{
$ticket = self::find($ticket_id);
// チケット が null の場合 有効期限切れとする
if (empty($ticket)) {
return true;
}
if (empty($today)) {
// $today が null の場合は、今日になる
$today = Carbon::today();
}
if ($today->lt($ticket->start_date) || $today->gt($ticket->end_date)) {
return true;
} else {
return false;
}
}
この関数は、「チケットが今日有効期限切れか?」から「チケットが第2引数の日に有効期限切れか?」という風に解釈が拡大されました。こうすることで、UnitTestの実行した瞬間の日付を考えずにUnitTestを実行できるようになります。
そして、Ticketデータも渡してやることで、さらにUnitTestがやりやすくなります。
有効期限切れかどうかを知りたいだけなので、「Ticketデータを取ってくること」は、本来の目的とずれるからです。
/**
* expired
* チケットが第2引数の日に有効期限切れか?
*
* @param Ticket $ticket
* @param Carbon $today null の場合は、今日になる
* @return bool 有効期限切れ true 有効期限は切れていない false
*/
public static function expired(Ticket $ticket = null, Carbon $today = null): bool
{
// チケット が null の場合 有効期限切れとする
if (empty($ticket)) {
return true;
}
if (empty($today)) {
// $today が null の場合は、今日になる
$today = Carbon::today();
}
if ($today->lt($ticket->start_date) || $today->gt($ticket->end_date)) {
return true;
} else {
return false;
}
}
第1引数を ticket_id
から Ticket
のデータそのものに変更しました。こうすることで、UnitTestの実行の際に、tickets テーブルにテストデータを入れておく必要もなくなります。そして、この関数の役割は、「有効かどうかを判定すること」のみに集中できるようになりました。
個人的には if 文は短く、カッコも少なくしたいので、最後こういう感じでまとめたいです。
/**
* expired
* チケットが第2引数の日に有効期限切れか?
*
* @param Ticket $ticket
* @param Carbon $today null の場合は、今日になる
* @return bool 有効期限切れ true 有効期限は切れていない false
*/
public static function expired(Ticket $ticket = null, Carbon $today = null): bool
{
if (empty($ticket)) return true; // チケット が null の場合 有効期限切れとする
if (empty($today)) $today = Carbon::today(); // $today が null の場合は、今日になる
if ($today->lt($ticket->start_date)) return true;
if ($today->gt($ticket->end_date)) return true;
return false;
}
この形であれば、UnitTestはこんな感じでできるでしょう!
/**
* testExpired
*
* @return void
*/
public function testExpired()
{
// 引数なしのときは有効期限切れ
$this->assertTrue(Ticket::expired());
$ticket = new Ticket;
$ticket->id = 1;
$ticket->start_date = '2022-07-01';
$ticket->end_date = '2022-07-31';
// 境界値のチェック
$today = new Carbon('2022-06-30');
$this->assertTrue(Ticket::expired($ticket, $today));
$today = new Carbon('2022-07-01');
$this->assertFalse(Ticket::expired($ticket, $today));
$today = new Carbon('2022-07-31');
$this->assertFalse(Ticket::expired($ticket, $today));
$today = new Carbon('2022-08-01');
$this->assertTrue(Ticket::expired($ticket, $today));
// 現在日付は、2022.8.17以後になるので有効期限切れ
$this->assertTrue(Ticket::expired($ticket));
}
最後の行の「現在日付は、2022.8.17以後になるので有効期限切れ」は、テストを作成した日が2022.8.17でした。そして、ここから先はずっと有効期限切れになるはずです!というおまけのテストでした。