関数の役割を研ぎ澄ます!Laravelで作った関数をUnitTestしやすくリファクタリングしよう!

工事中の迂回歩道
工事中の迂回歩道 フリー素材ぱくたそ(www.pakutaso.com)
  • URLをコピーしました!

Laravel8で開発をしています。
とある関数があります。チケットの有効期限切れをチェックする関数です。
ticketsテーブルには start_dateend_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でした。そして、ここから先はずっと有効期限切れになるはずです!というおまけのテストでした。

ツチノコテクノロジーに開発・保守を発注しませんか?

Laravel・Flutterの開発・保守をツチノコテクノロジーに発注しませんか?

まずはZOOMで打ち合わせ

お申し込みはこちら

ツチノコテクノロジーでは一緒に働く仲間を募集しています!

完全リモートで働きたい方へ!

詳しくは以下をご覧ください。

ツチノコテクノロジー採用サイト

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ツチノコテックアカデミアの記事は、社内で誰かが質問してくれたことに回答したときに、ついでに記載しています!(^^)/
みんなの悩みを共有すれば、きっと誰かの役に立つと信じて更新しています!

目次