Factory+Stubでテストデータを統一した話
何度かFactoryを書き直して気づいたのは、
「本番Eloquentが賢すぎるほど、テストがつらい」ということ。そこで、テスト専用のEloquent Stubを用意して、
FactoryはそのStubだけにぶら下げる運用に切り替えました。
目次
先に結論
Stubをつかった運用をこうやったって感じです。
- 本番Eloquentはテストで使わない。
- テストでは
Tests\Suite\Support\Models\Stub*を使う(接頭辞 Stub)。 - Factoryも Stubモデル専用(
StubSpotFactoryなど)。 - グローバルスコープ/イベント/複雑なキャストは一切持たない。
- テーブル名だけ合わせる(
protected $table = 'spots'など)。 - リレーションは必要最低限(or なし)。「速く・壊れない」を最優先。
- Factory(Eloquent):永続化が前提のテスト(Infra/DBクエリ/Repository/Controllerなど)
- Stub(プレーンPHP):永続化不要・ドメイン/UseCase のテスト
そして置き場も統一します。
tests/Suite/
├── Models/
│ ├── StubSpot.php
│ ├── StubCompany.php
│ └── StubArea.php
├── Factories/
│ ├── StubSpotFactory.php
│ └── StubCompanyFactory.php
├── Infrastructure/
│ └── Queries/
│ └── DbSpotListQueryTest.php
└── Application/
└── UseCases/
└── SearchSpotUseCaseTest.php本番Eloquentを使わずに、Stubモデル+Factoryだけでテストデータを作る話
最小実装(コピペでOK)
1) Stubモデル(テスト専用・超ミニマル)
<?php declare(strict_types=1);
namespace Tests\Suite\Common\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* StubSpot
* ------------------------------------------------------------
* テスト専用のEloquent(本番モデルの代替)。
* - グローバルスコープなし
* - イベントなし
* - キャスト最小限
*/
final class StubSpot extends Model
{
use HasFactory;
protected $table = 'spots'; // 本番と同じテーブル
public $timestamps = false; // 必要に応じて切替
protected $guarded = []; // テストでは一括代入OKに
protected static function newFactory()
{
return \Tests\Suite\Common\Factories\StubSpotFactory::new();
}
}2) Factory(Stub専用)
<?php declare(strict_types=1);
namespace Tests\Suite\Common\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Suite\Common\Models\StubSpot;
final class StubSpotFactory extends Factory
{
protected $model = StubSpot::class;
public function definition(): array
{
return [
'name' => $this->faker->company(),
'address' => $this->faker->address(),
'area_id' => 14,
'display' => 1,
'created_at' => now(),
'updated_at' => now(),
];
}
public function hidden(): self
{
return $this->state(fn() => ['display' => 0]);
}
}ポイント:newFactory() をStubモデル側でoverrideしておけば、StubSpot::factory() がそのまま使えます。Factory::guessFactoryNamesUsing() の設定は不要。
インフラ系テスト(DBを触るやつ)
<?php declare(strict_types=1);
namespace Tests\Suite\Infrastructure\Queries;
use Tests\TestCase;
use PHPUnit\Framework\Attributes\Large;
use Tests\Suite\Common\Models\StubSpot;
use Tool\General\Infrastructure\Queries\DbSpotListQuery;
#[Large]
final class DbSpotListQueryTest extends TestCase
{
public function test_表示対象のみ取得できる(): void
{
StubSpot::factory()->count(2)->create(['display' => 1]);
StubSpot::factory()->hidden()->create();
$q = new DbSpotListQuery();
$res = $q->search(areas: [14], keywords: '', page: 1, perPage: 10);
$this->assertSame(2, $res['total']);
$this->assertCount(2, $res['items']);
}
}
アプリ系テスト(DBを触らないやつ)
UseCaseは Stub(配列)で十分。DBは切る。
<?php declare(strict_types=1);
namespace Tests\Suite\Application\UseCases;
use Tests\TestCase;
use PHPUnit\Framework\Attributes\Small;
use Tool\General\Application\UseCases\Spot\{
SearchSpotUseCase, SearchSpotCommand
};
use Tool\General\Application\DTOs\Spot\SpotListResult;
use Tool\General\Domain\Queries\SpotListQuery;
#[Small]
final class SearchSpotUseCaseTest extends TestCase
{
public function test_resultの形が崩れない(): void
{
$query = new class implements SpotListQuery {
public function search(array $areas, string $keywords, int $page, int $perPage): array
{
return [
'total' => 2, 'page' => 1, 'perPage' => 10,
'items' => [
['id'=>1,'name'=>'A','address'=>'旭川','created'=>'2025-10-01'],
['id'=>2,'name'=>'B','address'=>'旭川','created'=>'2025-10-02'],
],
];
}
};
$uc = new SearchSpotUseCase($query);
$res = $uc->handle(new SearchSpotCommand([14], '', 1, 10));
$this->assertInstanceOf(SpotListResult::class, $res);
$this->assertSame(2, $res->total);
}
}なぜこれで楽になるか
- 本番モデルの賢さ(グローバルスコープ・イベント・Observer・複雑cast)を完全に回避
- Factoryの初期値や
state()をテスト都合で自由に設計できる - 破壊的変更(本番モデルの仕様変更)がテストに波及しない
- 「どっちのFactoryだっけ?」問題が消える(Stub専用しかない)
よくある疑問
Q. リレーションは?
A. 必要になってから最小で追加。belongsTo だけ足す→相手側も StubCompany を作る、でOK。無理に本番モデルへ寄せない。
Q. 外部キー制約は?
A. テストで Schema::disableForeignKeyConstraints() を使うか、Factoryで関連も一緒に作成。どちらでも。
まとめ
本番のEloquentモデルをそのままテストに使うと、
グローバルスコープやイベント、キャストなどの影響で
「本当に確認したい部分」までノイズが混ざってしまうことがあります。
そこでテストでは、本番モデルを完全に切り離し、
StubモデルとStubFactoryだけでデータを作る運用に変えました。
テーブル名だけ合わせて、余計な機能を全部削ったStubモデルを使えば、
Factoryの定義もシンプルになり、テストは速く・安定して・壊れにくくなります。
Eloquentをそのまま使うより、
テスト専用に“簡素化したコピー”を持つほうが、実務ではずっと快適。
迷ったら、まずは tests/Suite/Models/Stub〜 を作ってみる。
これでテスト環境がかなり整ったなと感じました。
今回は以上です!