開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. PHP >
  4. Laravel >
  5. 時限爆弾式テストを撲滅しよう
no-image

時限爆弾式テストを撲滅しよう

今回もテストコードについて書いていきます。

今回のテーマは時限爆弾式テストコードをなくそうです。

ある日時を境に突然masterブランチのCIのテストが落ち始めた、という経験はありませんか?
私はあります。
そのように現在日時に依って失敗するテストのことを本記事では時限爆弾式テストと呼称します。

仮想シナリオとしてはユーザーの有料会員登録の無料期間をチェックするAPIのテストを行うのもとします。

以下の環境でテストを行う事が前提条件です

  • PHP 8.1.9
  • Laravel Framework 8.83.23

ではさっそく見ていきましょう。

マイグレーション作成

まずはマイグレーションを書いていきます。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;

class CreateTestUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('test_users', function (Blueprint $table) {
            $table->bigIncrements('id')->comment('ID');
            $table->timestamp('free_start_at')->default('1970-01-01 00:00:01')->comment('無料期間開始日時');
            $table->timestamp('free_end_at')->default('2038-01-08 00:00:00')->comment('無料期間終了日時');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('test_users');
    }
}

モデル作成

次にモデルを書いていきます。

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;

/**
 * App\Models\TestUser
 *
 * @property int $id テストユーザーID
 * @property Carbon $free_start_at 無料開始期間
 * @property Carbon|null $free_end_at 無料終了期間
 * @property Carbon|null $created_at 作成日時
 * @property Carbon|null $updated_at 更新日時
 * @method static Builder|static newModelQuery()
 * @method static Builder|static newQuery()
 * @method static Builder|static query()
 * @mixin \Eloquent
 */
class TestUser extends Model
{
    use HasFactory;

    /**
     * @var string[]
     */
    protected $fillable = [
        'free_start_at',
        'free_end_at',
    ];

    /**
     * @var string[]
     */
    protected $dates = [
        'free_start_at',
        'free_end_at',
    ];
}

Factory作成

次にFactoryを書いていきます。

<?php

namespace App\Database\Factories;

use App\Models\TestUser;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;

class TestUserFactory extends Factory
{
    /**
     * モデルと対応するファクトリの名前
     *
     * @var string
     */
    protected $model = TestUser::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'free_start_at' => Carbon::now(),
            'free_end_at' => Carbon::now()->addMonth(),
        ];
    }
}

route作成

次にrouteを書いていきます。
今回はModel Ruote Bindingを利用しているものとします。

<?php

Route::get('/test/{testUser}', 'TestController@index')->name('test');

Controller作成

次にControllerを書いていきます。
現在日時と無料期間開始日時および無料期間終了日時を比べて無料期間かどうかをチェックするAPIです。

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\TestUser;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;

/**
 * @package App\Http\Controllers
 */
class TestController extends Controller
{
    /**
     * @param TestUser $testUser
     * @return JsonResponse
     */
    public function index(TestUser $testUser): JsonResponse
    {
        $now = Carbon::now();
        if (
                $testUser->free_start_at->gte($now) &&
                 $testUser->free_end_at->lte($now)
       ) {
            return response()->json(
                [
                    'message' => '無料期間外です。'
                ]
            );
        }
        return response()->json(
            [
                'message' => '無料期間中です。'
            ]
        );
    }
}

テスト作成

次にテストを書いていきます。
こちらは2022年10月5日に実行したとします。
testTestUserIsFreeのテストでは無料期間開始日時が2022年1月1日、無料期間終了日時が2023年1月1日になっていますので、無料期間内になります。
testTestUserIsNotFreeのテストでは無料期間開始日時が2022年12月1日、無料期間終了日時が2023年1月1日になっていますので、無料期間外になります。

<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\TestUser;
use Tests\TestCase;

/**
 * Class ExampleTest
 * @package Tests\Feature
 */
class ExampleTest extends TestCase
{
    /**
     * @testdox ユーザーが無料期間中か確認する
     */
    public function testTestUserIsFree(): void
    {
        /** @var TestUser $testUser */
        $testUser = TestUser::factory()->create(
            [
                'free_start_at' => '2021-01-01 00:00:00'
                'free_end_at' => '2023-01-01 00:00:00'
            ]
        );
        $testUserId = $testUser->id;
        $response = $this->get("/test/${testUserId}");

        $response->assertJson(
            [
                'message' => '無料期間中です。'
            ]
        );
    }

    /**
     * @testdox ユーザーが無料期間外が確認する
     */
    public function testTestUserIsNotFree(): void
    {
        /** @var TestUser $testUser */
        $testUser = TestUser::factory()->create(
            [
                'free_start_at' => '2022-12-31 00:00:00',
                'free_end_at' => '2023-01-01 00:00:00'
            ]
        );
        $testUserId = $testUser->id;
        $response = $this->get("/test/${testUserId}");

        $response->assertJson(
            [
                'message' => '無料期間外です。'
            ]
        );
    }
}

このテストでの問題点は以下です。

  • testTestUserIsFreeでは現在日時が2023年1月1日00時00分00秒まではテストが通るのですが、現在日時が2023年1月1日00時00分01秒になった時からテストが通らなくなります。
  • testTestUserIsNotFreeでは現在日時が2022年12月31日00時00分00秒まではテストが通るのですが、現在日時が2022年12月31日00時00分01秒になった時からテストが通らなくなります。

これが時限爆弾になります。
この時限爆弾を取っ払うためにCarbon::setTestNow()を使ってテスト中の現在日時を固定するようにしましょう。

<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\TestUser;
use Tests\TestCase;

/**
 * Class ExampleTest
 * @package Tests\Feature
 */
class ExampleTest extends TestCase
{
    /**
     * @return void
     */
    public function setUp(): void
    {
        parent::setUp();
        Carbon::setTestNow('2022-03-26 00:00:00');
    }


    /**
     * @testdox ユーザーが無料期間中か確認する
     */
    public function testTestUserIsFree(): void
    {
        /** @var TestUser $testUser */
        $testUser = TestUser::factory()->create(
            [
                'free_end_at' => '2023-01-01 00:00:00'
            ]
        );
        $testUserId = $testUser->id;
        $response = $this->get("/test/${testUserId}");

        $response->assertJson(
            [
                'message' => '無料期間中です。'
            ]
        );
    }

    /**
     * @testdox ユーザーが無料期間外か確認する
     */
    public function testTestUserIsNotFree(): void
    {
        /** @var TestUser $testUser */
        $testUser = TestUser::factory()->create(
            [
                'free_start_at' => '2022-12-31 00:00:00',
                'free_end_at' => '2023-01-01 00:00:00'
            ]
        );
        $testUserId = $testUser->id;
        $response = $this->get("/test/${testUserId}");

        $response->assertJson(
            [
                'message' => '無料期間外です。'
            ]
        );
    }
}

まとめ

Carbon::setTestNow()を使用することによって、時限爆弾式のテストコードをなくす方法を紹介しました。
時限爆弾式のテストコードは後々修正に困りますのでなくせるようにしましょう。

TOPに戻る