mcpField
L
laravel-testing

laravel-testing

github.com/leeoveryテスト・品質43177 回インストール31 週間前に更新
リポジトリ

laravel-testing について

Laravel Testingは、Laravel開発者向けのPest中心のテストスキルです。Arrange-Act-Assert、ファクトリーによる現実的なデータ作成、Actionの分離テスト、モックとnull driverの適切な利用を扱います。

主な機能

  1. 01Arrange-Act-Assertのテスト構成
  2. 02ActionとHTTP機能テスト
  3. 03ファクトリーによる現実的なテストデータ
  4. 04コンテナ経由で自作依存をモック
  5. 05null driverによる外部サービスのテスト

活用シーン

  1. 01Laravel Actionの焦点を絞ったテスト
  2. 02APIのリクエストからレスポンスまでの検証
  3. 03宣言的なファクトリーで読みやすいテストを作成
  4. 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 ドライバー、宣言的ファクトリー。

関連ガイド:

基本方針

テストは次のようにあるべきです。

  • 分離されている - 一度に一つのことをテストします
  • 信頼できる - 毎回一貫した結果が得られます
  • 保守しやすい - コードの変更に合わせて簡単に更新できます
  • 高速である - すぐにフィードバックが得られます
  • 現実的である - 固定値ではなくファクトリーを使います

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
自分が管理していないものをモックする必要がある場合

外部サービスをモックする必要がある場合は、抽象化を作成します。

  1. Manager パターンを使って サービスレイヤーを作成する
  2. ドライバーコントラクト(インターフェース)を定義する
  3. 実際のドライバーを実装する(外部 API をラップします)
  4. テスト用の null ドライバーを作成する
  5. 利便性のために ファサードを追加する

実装例の全体については、サービスを参照してください。

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);

コメント