laravel-testing について
Laravel Testingは、Laravel開発者向けのPest中心のテストスキルです。Arrange-Act-Assert、ファクトリーによる現実的なデータ作成、Actionの分離テスト、モックとnull driverの適切な利用を扱います。
主な機能
- 01Arrange-Act-Assertのテスト構成
- 02ActionとHTTP機能テスト
- 03ファクトリーによる現実的なテストデータ
- 04コンテナ経由で自作依存をモック
- 05null driverによる外部サービスのテスト
活用シーン
- 01Laravel Actionの焦点を絞ったテスト
- 02APIのリクエストからレスポンスまでの検証
- 03宣言的なファクトリーで読みやすいテストを作成
- 04ネットワーク呼び出しなしで外部サービスの結果を検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ leeovery/ claude-laravel --skill laravel-testingインストール後、Claude Code で/laravel-testingを入力して読み込まれたか確認します。
同梱ファイルreferences/testing-conventions.mdreferences/testing-factories.md
詳細
Laravel のテスト
Pest を使ったテストパターン:Arrange-Act-Assert、適切なモック、null ドライバー、宣言的ファクトリー。
関連ガイド:
- testing-conventions.md - テストファイルの構成と RESTful な順序
- testing-factories.md - 読みやすいテストのための宣言的ファクトリーメソッド
- validation-testing.md - フォームリクエストのバリデーションテスト
- アクション - ユニットテストのためのアクションパターン
- コントローラー - フィーチャーテストのためのコントローラーパターン
- DTO - DTO のテストファクトリー
- サービス - null ドライバーを使ったサービスレイヤー
基本方針
テストは次のようにあるべきです。
- 分離されている - 一度に一つのことをテストします
- 信頼できる - 毎回一貫した結果が得られます
- 保守しやすい - コードの変更に合わせて簡単に更新できます
- 高速である - すぐにフィードバックが得られます
- 現実的である - 固定値ではなくファクトリーを使います
Triple-A パターン
すべてのテストで Arrange-Act-Assert パターンに従います。
1. テスト環境を準備する
必要なデータと依存関係をすべて ファクトリー を使って設定します。
it('creates an order with items', function () {
// Arrange: Create the world state
$user = User::factory()->create();
$product = Product::factory()->active()->create(['price' => 1000]);
$data = CreateOrderData::from([
'customer_email' => '[email protected]',
'items' => [
['product_id' => $product->id, 'quantity' => 2],
],
]);
// Act: Perform the operation
$order = resolve(CreateOrderAction::class)($user, $data);
// Assert: Verify the results
expect($order)
->toBeInstanceOf(Order::class)
->and($order->items)->toHaveCount(1)
->and($order->total)->toBe(2000);
});
2. テスト対象を実行する
テストする 単一の操作 を実行します。
// ✅ Good - Single, clear action
$order = resolve(CreateOrderAction::class)($user, $data);
// ❌ Bad - Multiple actions mixed with assertions
$order = resolve(CreateOrderAction::class)($user, $data);
expect($order)->toBeInstanceOf(Order::class);
$order->refresh();
expect($order->total)->toBe(2000);
3. 結果を検証する
操作の 結果 を検証します。
// ✅ Good - Clear, focused assertions
expect($order)
->toBeInstanceOf(Order::class)
->and($order->status)->toBe(OrderStatus::Pending)
->and($order->items)->toHaveCount(2);
assertDatabaseHas('orders', [
'id' => $order->id,
'user_id' => $user->id,
]);
// ❌ Bad - Testing implementation details
expect($order->getAttribute('status'))->toBe('pending');
アクションのテスト
アクションは ドメインロジックの中心 であり、分離して徹底的にテストする必要があります。
基本的なアクションのテスト
use App\Actions\Order\CreateOrderAction;
use App\Data\CreateOrderData;
use App\Enums\OrderStatus;
use App\Models\User;
use function Pest\Laravel\assertDatabaseHas;
it('creates an order', function () {
// Arrange
$user = User::factory()->create();
$data = CreateOrderData::testFactory()->make([
'status' => OrderStatus::Pending,
]);
// Act
$order = resolve(CreateOrderAction::class)($user, $data);
// Assert
expect($order)->toBeInstanceOf(Order::class);
assertDatabaseHas('orders', [
'id' => $order->id,
'user_id' => $user->id,
'status' => OrderStatus::Pending->value,
]);
});
アクションのガードメソッドのテスト
it('throws exception when user has too many pending orders', function () {
// Arrange
$user = User::factory()
->has(Order::factory()->pending()->count(5))
->create();
$data = CreateOrderData::testFactory()->make();
// Act & Assert
expect(fn () => resolve(CreateOrderAction::class)($user, $data))
->toThrow(OrderException::class, 'Too many pending orders');
});
アクションの合成のテスト
重要なパターン: 依存関係が再帰的に解決されるよう、必ず resolve() を使ってコンテナからアクションを解決してください。swap() を使うと、依存関係をモックに置き換えられます。
use function Pest\Laravel\mock;
use function Pest\Laravel\swap;
it('processes order and sends notification', function () {
// Arrange
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
// Mock the dependency actions and swap them into the container
$calculateTotal = mock(CalculateOrderTotalAction::class);
$calculateTotal->shouldReceive('__invoke')
->once()
->with($order)
->andReturn(10000);
swap(CalculateOrderTotalAction::class, $calculateTotal);
$notifyOrder = mock(NotifyOrderCreatedAction::class);
$notifyOrder->shouldReceive('__invoke')
->once()
->with($order);
swap(NotifyOrderCreatedAction::class, $notifyOrder);
// Act - resolve() from container so mocked dependencies are injected
$result = resolve(ProcessOrderAction::class)($order);
// Assert
expect($result->total)->toBe(10000);
});
このパターンを使う理由:
resolve()によって、すべての依存関係を持つアクションがコンテナから取得されますswap()によって、コンテナ内の依存関係がモックに置き換えられます- コンテナが依存関係の再帰的な解決を自動的に処理します
- 依存関係に新たな依存関係が追加されても、テストは壊れません
モックのガイドライン
自分が管理するものだけをモックする
重要な原則: 自分が管理するコードだけをモックしてください。外部サービスを直接モックしてはいけません。
✅ 良い例 - 自分のアクションをモックする
use function Pest\Laravel\mock;
use function Pest\Laravel\swap;
// Mock an action you own and swap it into the container
$sendEmail = mock(SendWelcomeEmailAction::class);
$sendEmail->shouldReceive('__invoke')
->once()
->with(Mockery::type(User::class));
swap(SendWelcomeEmailAction::class, $sendEmail);
// Then resolve the action under test - it will receive the mocked dependency
$result = resolve(RegisterUserAction::class)($data);
✅ 応用例 - アサーションでモックの引数を検証する
クロージャーを指定した withArgs() を使って、渡される 正確なインスタンスと値 を検証します。
it('processes match with correct arguments', function () {
$matchAttempt = MatchAttempt::factory()->create();
$data = MatchData::testFactory()->make();
// Mock and verify exact arguments using expect() assertions
$mockAction = mock(CreateMatchResultAction::class);
$mockAction->shouldReceive('__invoke')
->once()
->withArgs(function (MatchAttempt $_matchAttempt, MatchData $_data) use ($data, $matchAttempt) {
// Verify the exact model instance is passed
expect($_matchAttempt->is($matchAttempt))->toBeTrue()
// Verify the exact DTO value is passed
->and($_data)->toBe($data->matches->first());
return true; // Return true to pass the assertion
});
swap(CreateMatchResultAction::class, $mockAction);
// Act
resolve(ProcessMatchAction::class)($matchAttempt, $data);
});
✅ 良い例 - 自分のサービスをモックする(ファサード経由)
// Mock your own service through its facade
Payment::shouldReceive('createPaymentIntent')
->once()
->with(10000, 'usd')
->andReturn(PaymentIntentData::from([
'id' => 'pi_test_123',
'status' => 'succeeded',
]));
❌ 悪い例 - 外部ライブラリーを直接モックする
// ❌ DON'T DO THIS - Mocking Stripe SDK directly
$stripe = Mockery::mock(\Stripe\StripeClient::class);
$stripe->shouldReceive('paymentIntents->create')
->andReturn(/* ... */);
// This is brittle and breaks when Stripe updates their SDK
自分が管理していないものをモックする必要がある場合
外部サービスをモックする必要がある場合は、抽象化を作成します。
- Manager パターンを使って サービスレイヤーを作成する
- ドライバーコントラクト(インターフェース)を定義する
- 実際のドライバーを実装する(外部 API をラップします)
- テスト用の null ドライバーを作成する
- 利便性のために ファサードを追加する
実装例の全体については、サービスを参照してください。
null ドライバーの使用
null ドライバーパターンを使うと、外部依存なしで 決定的かつ高速なテスト を実行できます。
it('processes payment successfully', function () {
// Arrange - Use null driver (configured in phpunit.xml or .env.testing)
Config::set('payment.default', 'null');
$order = Order::factory()->create(['total' => 10000]);
$data = PaymentData::from(['amount' => 10000, 'currency' => 'usd']);
// Act - No mocking needed, null driver returns test data
$payment = resolve(ProcessPaymentAction::class)($order, $data);
// Assert
expect($payment)
->toBeInstanceOf(Payment::class)
->and($payment->status)->toBe(PaymentStatus::Completed);
});
null ドライバーのメリット:
- モックが不要です
- 実行が高速です(ネットワーク呼び出しがありません)
- 結果が決定的です
- null ドライバーを拡張してエラーシナリオをテストできます
- 実際のドライバーとインターフェースが完全に一致します
エラーシナリオのテスト
特定のテストシナリオに合わせて null ドライバーを拡張します。
// tests/Fakes/FailingPaymentDriver.php
class FailingPaymentDriver implements PaymentDriver
{
public function createPaymentIntent(int $amount, string $currency): PaymentIntentData
{
throw PaymentException::failedToCharge('Card declined');
}
}
// In test
it('handles payment failure gracefully', function () {
$this->app->bind(PaymentManager::class, function () {
$manager = new PaymentManager($this->app);
$manager->extend('failing', fn () => new FailingPaymentDriver);
return $manager;
});
Config::set('payment.default', 'failing');
$order = Order::factory()->create();
$data = PaymentData::testFactory();
expect(fn () => resolve(ProcessPaymentAction::class)($order, $data))
->toThrow(PaymentException::class, 'Card declined');
});
ファクトリーの使用
ファクトリーを使うと 現実的でランダムなテストデータ を作成でき、テストの堅牢性が高まります。
モデルファクトリー
// Arrange with factories
$user = User::factory()->create();
$product = Product::factory()->active()->create();
$order = Order::factory()->for($user)->create();
// Factory with state
$pendingOrder = Order::factory()->pending()->create();
$paidOrder = Order::factory()->paid()->create();
// Factory with relationships
$user = User::factory()
->has(Order::factory()->count(3))
->create();
宣言的ファクトリーメソッド
重要な原則: ファクトリーメソッドの背後にデータベースの実装詳細を隠し、テストを 宣言的で読みやすく します。
// ❌ Bad - Database schema leaks into test
$calendar = Calendar::factory()->create([
'status' => 'accepted',
'reminder_sent_at' => null,
'approved_by' => User::factory()->create()->id,
'approved_at' => now(),
]);
// ✅ Good - Declarative and readable
$calendar = Calendar::factory()->accepted()->create();
→ 宣言的ファクトリーパターンの全容:testing-factories.md
DTO テストファクトリー
DTO では、一貫性のあるテストデータを用意するために テストファクトリー を提供します。
class CreateOrderData extends Data
{
public function __construct(
public string $customerEmail,
public OrderStatus $status,
public array $items,
) {}
public static function testFactory(): self
{
return new self(
customerEmail: fake()->email(),
status: OrderStatus::Pending,
items: [
[
'product_id' => Product::factory()->create()->id,
'quantity' => fake()->numberBetween(1, 5),
],
],
);
}
}
// Usage in tests
$data = CreateOrderData::testFactory();
テスト戦略
フィーチャーテスト(HTTP レイヤー)
リクエスト/レスポンスの一連のサイクルをテストします。
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;
it('creates an order via API', function () {
$user = User::factory()->create();
$product = Product::factory()->create();
$response = actingAs($user)
->postJson('/api/orders', [
'customer_email' => '[email protected]',
'items' => [
['product_id' => $product->id, 'quantity' => 2],
],
]);
$response->assertCreated()
->assertJsonStructure([
'data' => ['id', 'status', 'items'],
]);
});
ユニットテスト(アクション)
ドメインロジックを分離してテストします。
it('calculates order total correctly', function () {
$order = Order::factory()->create();
$order->items()->createMany([
['price' => 1000, 'quantity' => 2],
['price' => 1500, 'quantity' => 1],
]);
$total = resolve(CalculateOrderTotalAction::class)($order);
expect($total)->toBe(3500);
});
脆弱なテストを避ける
脆弱なテストは、動作が正しくても実装の変更によって失敗します。
脆弱なテストの兆候
- モックが多すぎる
- 実装の詳細をテストしている
- あちこちにハードコードされた値がある
- 手順の多い複雑なセットアップ
- リファクタリングでテストが失敗する
脆弱さを避ける方法
1. 可能な限り実際のインスタンスを使う
// ✅ Good - Use real instances
it('calculates order total', function () {
$order = Order::factory()->create();
$order->items()->createMany([
['price' => 1000, 'quantity' => 2],
['price' => 500, 'quantity' => 1],
]);
$total = resolve(CalculateOrderTotalAction::class)($order);
expect($total)->toBe(2500);
});
// ❌ Bad - Mock everything
it('calculates order total', function () {
$item1 = Mockery::mock(OrderItem::class);
$item1->shouldReceive('getPrice')->andReturn(1000);
// ... too much mocking
});
2. 実装ではなく動作をテストする
// ✅ Good - Test the behavior
it('sends welcome email when user registers', function () {
Mail::fake();
$data = RegisterUserData::testFactory();
$user = resolve(RegisterUserAction::class)($data);
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
});
// ❌ Bad - Test implementation details
it('sends welcome email when user registers', function () {
$mailer = Mockery::mock(Mailer::class);
$mailer->shouldReceive('send')
->with(Mockery::on(function ($email) {
return $email->template === 'emails.welcome';
}));
// Too specific, breaks if template name changes
});
3. ハードコードされたデータの代わりにファクトリーを使う
// ✅ Good - Use factories
$user = User::factory()->create();
$data = ProfileData::testFactory();
// ❌ Bad - Hardcoded data
$data = new ProfileData(
firstName: 'John',
lastName: 'Doe',
phone: '555-1234',
bio: 'Test bio',
);
4. モックを最小限にする
経験則: データではなく、連携するオブジェクトをモックします。
// ✅ Good - Mock the notification service (collaborator)
$notifier = mock(NotificationService::class);
$notifier->shouldReceive('send')->once();
swap(NotificationService::class, $notifier);
resolve(ShipOrderAction::class)($order);
// ❌ Bad - Mock the data (order, user)
$order = Mockery::mock(Order::class);
// ... mocking data objects makes test brittle
よく使われるテストパターン
状態遷移のテスト
it('transitions order from pending to paid', function () {
$order = Order::factory()->pending()->create();
resolve(MarkOrderAsPaidAction::class)($order);
expect($order->fresh()->status)->toBe(OrderStatus::Paid)
->and($order->fresh()->paid_at)->not->toBeNull();
});
リレーションシップのテスト
it('creates order with items', function () {
$user = User::factory()->create();
$products = Product::factory()->count(3)->create();
$data = CreateOrderData::from([
'customer_email' => '[email protected]',
'items' => $products->map(fn ($p) => [
'product_id' => $p->id,
'quantity' => 2,
])->all(),
]);
$order = resolve(CreateOrderAction::class)($user, $data);
expect($order->items)->toHaveCount(3);
});
トランザクションのテスト
it('rolls back transaction on failure', function () {
$user = User::factory()->create();
$data = CreateOrderData::from([
'customer_email' => '[email protected]',
'items' => [
['product_id' => 99999, 'quantity' => 1], // Non-existent product
],
]);
expect(fn () => resolve(CreateOrderAction::class)($user, $data))
->toThrow(Exception::class);
assertDatabaseCount('orders', 0);
assertDatabaseCount('order_items', 0);
});
メール/通知のテスト
use Illuminate\Support\Facades\Mail;
it('sends welcome email to new user', function () {
Mail::fake();
$data = RegisterUserData::testFactory();
$user = resolve(RegisterUserAction::class)($data);
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
});
ジョブのテスト
use Illuminate\Support\Facades\Queue;
it('dispatches job to process order', function () {
Queue::fake();
$order = Order::factory()->create();
resolve(ProcessOrderAction::class)($order);
Queue::assertPushed(ProcessOrderJob::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
});
ベストプラクティスのまとめ
✅ 推奨事項
- トリプルAパターンに従う - 準備(Arrange)、実行(Act)、検証(Assert)
- テストデータにはファクトリーを使う
- 宣言的なファクトリーメソッドを作成する -
['status' => 'accepted']ではなくCalendar::factory()->accepted() - アクションを分離してテストする - ドメインロジックをユニットテストする
- 自分で管理するものをモックする - アクションや、自分で制御するサービス
- 外部サービスをモックする必要がある場合は、抽象化を作成する
- 外部サービスのテストにはヌルドライバーを使う
- 実装ではなく動作をテストする
- テストをシンプルに保つ - 1つのテストにつき1つの概念
- 一貫性のあるデータにはDTO テストファクトリーを使う
❌ 避けるべきこと
- 外部ライブラリをモックする - 代わりにサービスレイヤーを作成する
- テストデータをハードコードする - ファクトリーを使う
- データベーススキーマをテストに持ち込む - 宣言的なファクトリーメソッドを使う
- 実装の詳細をテストする - 動作をテストする
- 脆弱なテストを作成する - モックが多すぎる、または具体的すぎる
- ファクトリーを使わない - モデルと DTO には常にファクトリーを使う
- 準備と実行を混在させる - 分けておく
- 過剰にモックする - 可能な限り実際のインスタンスを使う
クイックリファレンス
テストの構造
it('does something', function () {
// Arrange - Set up the world with declarative factories
$model = Model::factory()->active()->create();
$data = Data::testFactory();
// Act - Perform the operation
$result = resolve(Action::class)($model, $data);
// Assert - Verify the results
expect($result)->/* assertions */;
});
モックのパターン
use function Pest\Laravel\mock;
use function Pest\Laravel\swap;
// Mock a dependency action
$mockAction = mock(YourDependencyAction::class);
$mockAction->shouldReceive('__invoke')
->once()
->with(/* expected params */)
->andReturn(/* return value */);
// Swap into container
swap(YourDependencyAction::class, $mockAction);
// Resolve action under test - container injects mocked dependencies
$result = resolve(ActionUnderTest::class)(/* params */);
データベースのアサーション
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\assertDatabaseCount;
assertDatabaseHas('orders', ['id' => $order->id]);
assertDatabaseCount('orders', 1);