時限爆弾式テストを撲滅しよう
今回もテストコードについて書いていきます。
今回のテーマは時限爆弾式テストコードをなくそうです。
ある日時を境に突然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()を使用することによって、時限爆弾式のテストコードをなくす方法を紹介しました。
時限爆弾式のテストコードは後々修正に困りますのでなくせるようにしましょう。