ワンクリックで
laravel-testing
Comprehensive testing patterns with Pest. Use when writing or modifying tests, mocking, factories, or test patterns.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Comprehensive testing patterns with Pest. Use when writing or modifying tests, mocking, factories, or test patterns.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Form request validation and comprehensive validation testing. Use when creating or modifying form requests, validation rules, or validation tests.
Feature module pattern organizing domain logic into queries, mutations, and actions. Use when implementing data fetching with filters, API mutations with loading states, business logic with UI feedback, or organizing domain-specific code.
File-based routing with page patterns for lists, details, and navigation. Use when creating pages, defining page meta (permissions, layouts), implementing list/detail patterns, or setting up breadcrumbs and headers.
Foundational architecture for Nuxt 4 + Vue 3 + Nuxt UI applications. Use when starting new projects, understanding project structure, or making architectural decisions about directory organization, technology choices, and pattern selection.
Vue component patterns with Composition API and script setup. Use when creating components, understanding script setup order convention, organizing component directories, or implementing component patterns like slideovers, modals, and tables.
Creating custom Vue composables with proper patterns. Use when building reusable stateful logic, shared state management, or encapsulating feature-specific behavior.
| name | laravel-testing |
| description | Comprehensive testing patterns with Pest. Use when writing or modifying tests, mocking, factories, or test patterns. |
Testing patterns with Pest: Arrange-Act-Assert, proper mocking, null drivers, declarative factories.
Related guides:
Testing should be:
Every test should follow the Arrange-Act-Assert pattern:
Set up all the data and dependencies needed using factories:
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' => 'customer@example.com',
'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);
});
Perform the single operation you're testing:
// ✅ 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);
Verify the outcomes of your action:
// ✅ 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');
Actions are the heart of your domain logic and should be thoroughly tested in isolation.
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');
});
Critical pattern: Always resolve actions from the container using resolve() so dependencies are recursively resolved. Use swap() to replace dependencies with mocked versions.
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);
});
Why this pattern:
resolve() ensures the action is pulled from the container with all dependenciesswap() replaces the dependency in the container with your mockCritical principle: Only mock code that you control. Never mock external services directly.
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);
Use withArgs() with a closure to verify the exact instances and values being passed:
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
If you find yourself needing to mock an external service, create an abstraction:
See Services for complete implementation examples.
The null driver pattern provides deterministic, fast tests without external dependencies:
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);
});
Benefits of null drivers:
Extend the null driver for specific test scenarios:
// 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()->make();
expect(fn () => resolve(ProcessPaymentAction::class)($order, $data))
->toThrow(PaymentException::class, 'Card declined');
});
Factories create realistic, randomized test data that makes tests more robust.
// 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();
Critical principle: Make tests declarative and readable by hiding database implementation details behind factory methods.
// ❌ 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();
→ Complete declarative factory patterns: testing-factories.md
DTOs use the HasTestFactory trait with DataTestFactory base class — mirroring Laravel's model factory API for Data objects.
// Create DTO with defaults
$data = CreateOrderData::testFactory()->make();
// Create DTO with overrides
$data = CreateOrderData::testFactory()->make([
'status' => OrderStatus::Pending,
]);
// Create collection
$items = OrderItemData::testFactory()->collect(count: 3);
See DTOs for test factory setup and implementation.
Test the complete request/response cycle:
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' => 'test@example.com',
'items' => [
['product_id' => $product->id, 'quantity' => 2],
],
]);
$response->assertCreated()
->assertJsonStructure([
'data' => ['id', 'status', 'items'],
]);
});
Test domain logic in isolation:
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);
});
Brittle tests break when implementation changes, even if behavior is correct.
// ✅ 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
});
// ✅ Good - Test the behavior
it('sends welcome email when user registers', function () {
Mail::fake();
$data = RegisterUserData::testFactory()->make();
$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
});
// ✅ Good - Use factories
$user = User::factory()->create();
$data = ProfileData::testFactory()->make();
// ❌ Bad - Hardcoded data
$data = new ProfileData(
firstName: 'John',
lastName: 'Doe',
phone: '555-1234',
bio: 'Test bio',
);
Rule of thumb: Mock collaborators, not data.
// ✅ 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' => 'test@example.com',
'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' => 'test@example.com',
'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);
});
it('does something', function () {
// Arrange - Set up the world with declarative factories
$model = Model::factory()->active()->create();
$data = Data::testFactory()->make();
// 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);