🐘

なぜPHP標準関数のdateではなくCarbonを使うのか?

2023/11/25に公開

表題の通りです。

なぜ PHP 標準関数の date ではなく Carbon を使うのか?を順を追って説明します。

答え

結論から先に述べておくと、Carbon を使う最も高いモチベーションは、UnitTest の際に副作用が生じる。モック化して副作用を消したいが、モック化 するのが難しいからです。

どういうことか説明していきます。

修正前コード

以下は実際にdateを使っているクラスの例です。

ProgramService.php
<?php

namespace App\Services\Programs;

use App\Models\Notifyprogram;
use App\Models\Personality;
use App\Models\Program;

class ProgramService
{
    //中略

    /**
     * 曜日を返す.
     *
     * @return array
     */
    public function getWeekDays()
    {
        $weekDays = ['0' => 'MonDay', '1' => 'Tuesday', '2' => 'Wednesday', '3' => 'Thursday', '4' => 'FriDay', '5' => 'Saturday', '6' => 'SunDay'];

        return $weekDays;
    }

    /**
     * 今日の曜日を返す
     * バッチは0が月曜日で6が日曜日なので、1を引く。負数は6にする.
     *
     * @return int
     */
    public function getTargetDay()
    {
        $today = date('w');
        $today = ($today - 1);
        if ($today < 0) {
            $today = 6;
        }

        return $today;
    }
}

getTargetDayはとある事情により、$todayを-1 した日付を返します。

この処理の必要性は、DB には Python で生成されたデータが保存されており、PHP は日曜日始まり、Python は月曜日始まりで始まることに起因しています。

PHP 側の日付を-1 することで Python の曜日に合わせているという感じです。

一方でテストコードは以下のようになっています。

test('getTargetDay is Valid', function () {
    $sut = new ProgramService();
    $exp = date('w');
    $exp = ($exp - 1);
    if ($exp < 0) {
        $exp = 6;
    }
    $act = $sut->getTargetDay();
    expect($act === $exp)->toBeTrue();
});

これまた良くないコードになってしまっています。

プロダクション側と同じようなコードを書くことで無理やりアサーションしようとしています。

そしてこのコードには重大な問題があります。

日曜日にテストが実行された場合にしか if の中を通らないという点です。

この処理の背景が Python との曜日の歪みを埋めるために生まれており、if の中が処理として重要な部分なはずです。

しかしここを検証できていないテストコードにあまり意味がありません。

というわけでdata('w')をモックして、0 または 0 以外を返す状態を検証するのがよさそうです。

…が調べたところ困ったことに PHP 標準関数の date をモックするのは困難で、非常にめんどくさそうです。

一方でCarbonであればテストコード実行を想定しているので、2 行で簡単にモックできます。

修正後コード

ではdateCarbonに置き換えたコードがこちらです。

ProgramService.php
<?php

namespace App\Services\Programs;

use App\Models\Notifyprogram;
use App\Models\Personality;
use App\Models\Program;
use Carbon\Carbon;

class ProgramService
{
    //中略

    /**
     * 曜日を返す.
     *
     * @return array
     */
    public function getWeekDays()
    {
        $weekDays = ['0' => 'MonDay', '1' => 'Tuesday', '2' => 'Wednesday', '3' => 'Thursday', '4' => 'FriDay', '5' => 'Saturday', '6' => 'SunDay'];

        return $weekDays;
    }

    /**
     * 今日の曜日を返す
     * バッチは0が月曜日で6が日曜日なので、1を引く。負数は6にする.
     *
     * @return int
     */
    public function getTargetDay()
    {
        $Carbon::now()->dayOfWeek;
        $today = ($today - 1);
        if ($today < 0) {
            $today = 6;
        }

        return $today;
    }
}

そしてテストコード側はCarbonをモックしつつ、C1網羅したテストが書けていそうです。

test('getTargetDay returns Valid PythonDate When date func resutns other than 0', function () {
    $sut = new ProgramService();
    $knownDate = Carbon::create(2023, 11, 20, 0, 0, 0); //とある月曜日
    Carbon::setTestNow($knownDate);
    $act = $sut->getTargetDay();
    $exp = 0;
    expect($act === $exp)->toBeTrue();
});

test('getTargetDay returns Valid PythonDate When date func resutns 0', function () {
    $sut = new ProgramService();
    $knownDate = Carbon::create(2023, 11, 19, 0, 0, 0); //とある日曜日
    Carbon::setTestNow($knownDate);
    $act = $sut->getTargetDay();
    $exp = 6;
    expect($act === $exp)->toBeTrue();
});

Mutation Testing にて適切に修正されたことを確認

Mutation Testing とは

こちらの記事を参照

https://zenn.dev/bs_kansai/articles/8fa18a5a94ec77

上記記事は JavaScript の Mutator であるStrykerを使っていますが、今回は PHP の Mutator であるInfectionを使っています。

https://infection.github.io/

Infectionについては今度 PHP カンファレンス関西 2024 にてお話しする予定です。よければぜひ聴きにきてください。

https://fortee.jp/phpcon-kansai2024/proposal/8daa1c68-69b1-458a-9f3a-0c9a86e7843e

適切に修正されたことを確認

修正前は以下のように、if の中の変異を検出できていません。

ですが、修正後は変異が Kill されたことが確認できました。

おわりに

実はこの記事を書くきっかけは、Mutation Testing を実行したところこの記事の例のコード部分が引っかかったからです。

その意味でやはり Mutation Testing は有効なテストコード作成に寄与していると言えそうです。

Discussion