laravel-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLaravel Testing
Laravel 测试
Testing patterns with Pest: Arrange-Act-Assert, proper mocking, null drivers, declarative factories.
Related guides:
- testing-conventions.md - Test file structure and RESTful ordering
- testing-factories.md - Declarative factory methods for readable tests
- validation-testing.md - Form request validation testing
- Actions - Action pattern for unit testing
- Controllers - Controller patterns for feature testing
- DTOs - DTO test factories
- Services - Service layer with null drivers
基于Pest的测试模式:Arrange-Act-Assert(准备-执行-断言)、正确的Mocking、空驱动、声明式工厂。
相关指南:
- testing-conventions.md - 测试文件结构与RESTful排序
- testing-factories.md - 用于可读性测试的声明式工厂方法
- validation-testing.md - 表单请求验证测试
- Actions - 用于单元测试的Action模式
- Controllers - 用于功能测试的控制器模式
- DTOs - DTO测试工厂
- Services - 带空驱动的服务层
Philosophy
理念
Testing should be:
- Isolated - Test one thing at a time
- Reliable - Consistent results every time
- Maintainable - Easy to update when code changes
- Fast - Quick feedback loop
- Realistic - Use factories, not hardcoded values
测试应具备以下特性:
- 隔离性 - 每次只测试一件事
- 可靠性 - 每次运行都得到一致结果
- 可维护性 - 代码变更时易于更新
- 高效性 - 快速反馈循环
- 真实性 - 使用工厂生成数据,而非硬编码值
The Triple-A Pattern
Triple-A 模式
Every test should follow the Arrange-Act-Assert pattern:
每个测试都应遵循**Arrange-Act-Assert(准备-执行-断言)**模式:
1. Arrange the World
1. 准备测试环境
Set up all the data and dependencies needed using factories:
php
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);
});使用工厂设置所需的所有数据和依赖:
php
it('creates an order with items', function () {
// Arrange: 创建测试环境状态
$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: 执行操作
$order = resolve(CreateOrderAction::class)($user, $data);
// Assert: 验证结果
expect($order)
->toBeInstanceOf(Order::class)
->and($order->items)->toHaveCount(1)
->and($order->total)->toBe(2000);
});2. Act on the World
2. 执行测试操作
Perform the single operation you're testing:
php
// ✅ 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);执行你要测试的单一操作:
php
// ✅ 良好实践 - 单一、清晰的操作
$order = resolve(CreateOrderAction::class)($user, $data);
// ❌ 不良实践 - 操作与断言混合
$order = resolve(CreateOrderAction::class)($user, $data);
expect($order)->toBeInstanceOf(Order::class);
$order->refresh();
expect($order->total)->toBe(2000);3. Assert on the Results
3. 断言测试结果
Verify the outcomes of your action:
php
// ✅ 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');验证操作的输出结果:
php
// ✅ 良好实践 - 清晰、聚焦的断言
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,
]);
// ❌ 不良实践 - 测试实现细节
expect($order->getAttribute('status'))->toBe('pending');Testing Actions
测试Actions
Actions are the heart of your domain logic and should be thoroughly tested in isolation.
Actions是领域逻辑的核心,应进行彻底的隔离测试。
Basic Action Test
基础Action测试
php
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,
]);
});php
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,
]);
});Testing Action Guard Methods
测试Action的守卫方法
php
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');
});php
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');
});Testing Action Composition
测试Action组合
Critical pattern: Always resolve actions from the container using so dependencies are recursively resolved. Use to replace dependencies with mocked versions.
resolve()swap()php
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:
- ensures the action is pulled from the container with all dependencies
resolve() - replaces the dependency in the container with your mock
swap() - Container handles recursive dependency resolution automatically
- If a dependency adds a new dependency, your tests don't break
关键模式: 始终使用从容器中解析Actions,确保依赖被递归解析。使用将依赖替换为Mock版本。
resolve()swap()php
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依赖Action并替换到容器中
$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(),确保注入Mock依赖
$result = resolve(ProcessOrderAction::class)($order);
// Assert
expect($result->total)->toBe(10000);
});该模式的优势:
- 确保从容器中获取带有所有依赖的Action
resolve() - 将容器中的依赖替换为你的Mock
swap() - 容器自动处理递归依赖解析
- 如果依赖新增了子依赖,你的测试不会中断
Mocking Guidelines
Mocking 指南
Only Mock What You Own
仅Mock你拥有的代码
Critical principle: Only mock code that you control. Never mock external services directly.
关键原则: 仅Mock你控制的代码。永远不要直接Mock外部服务。
✅ Good - Mock Your Own Actions
✅ 良好实践 - Mock你自己的Actions
php
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);php
use function Pest\Laravel\mock;
use function Pest\Laravel\swap;
// Mock你自己的Action并替换到容器中
$sendEmail = mock(SendWelcomeEmailAction::class);
$sendEmail->shouldReceive('__invoke')
->once()
->with(Mockery::type(User::class));
swap(SendWelcomeEmailAction::class, $sendEmail);
// 然后解析要测试的Action - 它会接收Mock依赖
$result = resolve(RegisterUserAction::class)($data);✅ Advanced - Verify Mock Arguments with Assertions
✅ 进阶 - 用断言验证Mock参数
Use with a closure to verify the exact instances and values being passed:
withArgs()php
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);
});使用和闭包来验证传递的精确实例和值:
withArgs()php
it('processes match with correct arguments', function () {
$matchAttempt = MatchAttempt::factory()->create();
$data = MatchData::testFactory()->make();
// Mock并使用expect()断言验证精确参数
$mockAction = mock(CreateMatchResultAction::class);
$mockAction->shouldReceive('__invoke')
->once()
->withArgs(function (MatchAttempt $_matchAttempt, MatchData $_data) use ($data, $matchAttempt) {
// 验证传递的是精确的模型实例
expect($_matchAttempt->is($matchAttempt))->toBeTrue()
// 验证传递的是精确的DTO值
->and($_data)->toBe($data->matches->first());
return true; // 返回true以通过断言
});
swap(CreateMatchResultAction::class, $mockAction);
// Act
resolve(ProcessMatchAction::class)($matchAttempt, $data);
});✅ Good - Mock Your Own Services (via Facade)
✅ 良好实践 - Mock你自己的服务(通过Facade)
php
// Mock your own service through its facade
Payment::shouldReceive('createPaymentIntent')
->once()
->with(10000, 'usd')
->andReturn(PaymentIntentData::from([
'id' => 'pi_test_123',
'status' => 'succeeded',
]));php
// 通过Facade Mock你自己的服务
Payment::shouldReceive('createPaymentIntent')
->once()
->with(10000, 'usd')
->andReturn(PaymentIntentData::from([
'id' => 'pi_test_123',
'status' => 'succeeded',
]));❌ Bad - Mocking External Libraries Directly
❌ 不良实践 - 直接Mock外部库
php
// ❌ 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 SDKphp
// ❌ 不要这样做 - 直接Mock Stripe SDK
$stripe = Mockery::mock(\Stripe\StripeClient::class);
$stripe->shouldReceive('paymentIntents->create')
->andReturn(/* ... */);
// 这种方式很脆弱,Stripe更新SDK时会失效When You Need to Mock Something You Don't Own
当你需要Mock非自有代码时
If you find yourself needing to mock an external service, create an abstraction:
- Create a Service Layer with the Manager pattern
- Define a Driver Contract (interface)
- Implement the Real Driver (wraps external API)
- Create a Null Driver for testing
- Add a Facade for convenience
See Services for complete implementation examples.
如果你发现需要Mock外部服务,创建一个抽象层:
- 创建服务层,使用Manager模式
- 定义驱动契约(接口)
- 实现真实驱动(封装外部API)
- 创建空驱动用于测试
- 添加Facade以方便使用
完整实现示例请参见 Services。
Using Null Drivers
使用空驱动
The null driver pattern provides deterministic, fast tests without external dependencies:
php
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:
- No mocking required
- Fast execution (no network calls)
- Deterministic results
- Can test error scenarios by extending null driver
- Matches real driver interface exactly
空驱动模式提供可预测、快速的测试,无需外部依赖:
php
it('processes payment successfully', function () {
// Arrange - 使用空驱动(在phpunit.xml或.env.testing中配置)
Config::set('payment.default', 'null');
$order = Order::factory()->create(['total' => 10000]);
$data = PaymentData::from(['amount' => 10000, 'currency' => 'usd']);
// Act - 无需Mock,空驱动返回测试数据
$payment = resolve(ProcessPaymentAction::class)($order, $data);
// Assert
expect($payment)
->toBeInstanceOf(Payment::class)
->and($payment->status)->toBe(PaymentStatus::Completed);
});空驱动的优势:
- 无需Mock
- 执行速度快(无网络请求)
- 结果可预测
- 可通过扩展空驱动测试错误场景
- 与真实驱动接口完全匹配
Testing Error Scenarios
测试错误场景
Extend the null driver for specific test scenarios:
php
// 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');
});扩展空驱动以测试特定场景:
php
// tests/Fakes/FailingPaymentDriver.php
class FailingPaymentDriver implements PaymentDriver
{
public function createPaymentIntent(int $amount, string $currency): PaymentIntentData
{
throw PaymentException::failedToCharge('Card declined');
}
}
// 在测试中
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');
});Using Factories
使用工厂
Factories create realistic, randomized test data that makes tests more robust.
工厂生成真实、随机的测试数据,让测试更健壮。
Model Factories
模型工厂
php
// 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();php
// 使用工厂准备数据
$user = User::factory()->create();
$product = Product::factory()->active()->create();
$order = Order::factory()->for($user)->create();
// 带状态的工厂
$pendingOrder = Order::factory()->pending()->create();
$paidOrder = Order::factory()->paid()->create();
// 带关联关系的工厂
$user = User::factory()
->has(Order::factory()->count(3))
->create();Declarative Factory Methods
声明式工厂方法
Critical principle: Make tests declarative and readable by hiding database implementation details behind factory methods.
php
// ❌ 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
关键原则: 通过工厂方法隐藏数据库实现细节,让测试声明式且易读。
php
// ❌ 不良实践 - 数据库架构暴露到测试中
$calendar = Calendar::factory()->create([
'status' => 'accepted',
'reminder_sent_at' => null,
'approved_by' => User::factory()->create()->id,
'approved_at' => now(),
]);
// ✅ 良好实践 - 声明式且易读
$calendar = Calendar::factory()->accepted()->create();→ 完整的声明式工厂模式:testing-factories.md
DTO Test Factories
DTO测试工厂
DTOs should provide test factories for consistent test data:
php
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();DTO应提供测试工厂以确保测试数据一致:
php
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),
],
],
);
}
}
// 在测试中使用
$data = CreateOrderData::testFactory();Testing Strategy
测试策略
Feature Tests (HTTP Layer)
功能测试(HTTP层)
Test the complete request/response cycle:
php
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'],
]);
});测试完整的请求/响应周期:
php
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'],
]);
});Unit Tests (Actions)
单元测试(Actions)
Test domain logic in isolation:
php
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);
});隔离测试领域逻辑:
php
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);
});Avoiding Brittle Tests
避免脆弱的测试
Brittle tests break when implementation changes, even if behavior is correct.
当实现细节变更时,脆弱的测试会失效,即使行为是正确的。
Signs of Brittle Tests
脆弱测试的迹象
- Too many mocks
- Testing implementation details
- Hardcoded values everywhere
- Complex setup with many steps
- Tests break with refactoring
- 过多的Mocks
- 测试实现细节
- 到处都是硬编码值
- 复杂的多步骤准备
- 重构时测试失效
How to Avoid Brittleness
如何避免脆弱性
1. Use Real Instances When Possible
1. 尽可能使用真实实例
php
// ✅ 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
});php
// ✅ 良好实践 - 使用真实实例
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);
});
// ❌ 不良实践 - Mock所有东西
it('calculates order total', function () {
$item1 = Mockery::mock(OrderItem::class);
$item1->shouldReceive('getPrice')->andReturn(1000);
// ... 过多的Mock
});2. Test Behavior, Not Implementation
2. 测试行为,而非实现
php
// ✅ 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
});php
// ✅ 良好实践 - 测试行为
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);
});
});
// ❌ 不良实践 - 测试实现细节
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';
}));
// 过于具体,模板名称变更时会失效
});3. Use Factories Instead of Hardcoded Data
3. 使用工厂而非硬编码数据
php
// ✅ 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',
);php
// ✅ 良好实践 - 使用工厂
$user = User::factory()->create();
$data = ProfileData::testFactory();
// ❌ 不良实践 - 硬编码数据
$data = new ProfileData(
firstName: 'John',
lastName: 'Doe',
phone: '555-1234',
bio: 'Test bio',
);4. Minimize Mocking
4. 最小化Mock
Rule of thumb: Mock collaborators, not data.
php
// ✅ 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经验法则: Mock协作对象,而非数据。
php
// ✅ 良好实践 - Mock通知服务(协作对象)
$notifier = mock(NotificationService::class);
$notifier->shouldReceive('send')->once();
swap(NotificationService::class, $notifier);
resolve(ShipOrderAction::class)($order);
// ❌ 不良实践 - Mock数据(订单、用户)
$order = Mockery::mock(Order::class);
// ... Mock数据对象会让测试变得脆弱Common Testing Patterns
常见测试模式
Testing State Transitions
测试状态转换
php
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();
});php
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();
});Testing Relationships
测试关联关系
php
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);
});php
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);
});Testing Transactions
测试事务
php
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);
});php
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], // 不存在的产品
],
]);
expect(fn () => resolve(CreateOrderAction::class)($user, $data))
->toThrow(Exception::class);
assertDatabaseCount('orders', 0);
assertDatabaseCount('order_items', 0);
});Testing Email/Notifications
测试邮件/通知
php
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);
});
});php
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);
});
});Testing Jobs
测试任务
php
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;
});
});php
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;
});
});Best Practices Summary
最佳实践总结
✅ Do This
✅ 推荐做法
- Follow triple-A pattern - Arrange, Act, Assert
- Use factories for all test data
- Create declarative factory methods - not
Calendar::factory()->accepted()['status' => 'accepted'] - Test actions in isolation - Unit test your domain logic
- Mock what you own - Actions, services you control
- Create abstractions when you need to mock external services
- Use null drivers for external service testing
- Test behavior, not implementation
- Keep tests simple - One concept per test
- Use DTO test factories for consistent data
- 遵循Triple-A模式 - 准备、执行、断言
- 使用工厂生成所有测试数据
- 创建声明式工厂方法 - 使用而非
Calendar::factory()->accepted()['status' => 'accepted'] - 隔离测试Actions - 单元测试领域逻辑
- Mock你拥有的代码 - 你控制的Actions、服务
- 创建抽象层当需要Mock外部服务时
- 使用空驱动测试外部服务
- 测试行为,而非实现
- 保持测试简单 - 每个测试一个概念
- 使用DTO测试工厂确保数据一致
❌ Don't Do This
❌ 不推荐做法
- Mock external libraries - Create service layer instead
- Hardcode test data - Use factories
- Leak database schema into tests - Use declarative factory methods
- Test implementation details - Test behavior
- Create brittle tests - Too many mocks, too specific
- Skip factories - Always use factories for models and DTOs
- Mix arrange and act - Keep them separate
- Over-mock - Use real instances when possible
- Mock外部库 - 改为创建服务层
- 硬编码测试数据 - 使用工厂
- 将数据库架构暴露到测试中 - 使用声明式工厂方法
- 测试实现细节 - 测试行为
- 创建脆弱的测试 - 过多Mock、过于具体
- 跳过工厂 - 始终为模型和DTO使用工厂
- 混合准备与执行步骤 - 保持两者分离
- 过度Mock - 尽可能使用真实实例
Quick Reference
快速参考
Test Structure
测试结构
php
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 */;
});php
it('does something', function () {
// Arrange - 使用声明式工厂准备测试环境
$model = Model::factory()->active()->create();
$data = Data::testFactory();
// Act - 执行操作
$result = resolve(Action::class)($model, $data);
// Assert - 验证结果
expect($result)->/* 断言 */;
});Mocking Pattern
Mocking模式
php
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 */);php
use function Pest\Laravel\mock;
use function Pest\Laravel\swap;
// Mock依赖Action
$mockAction = mock(YourDependencyAction::class);
$mockAction->shouldReceive('__invoke')
->once()
->with(/* 预期参数 */)
->andReturn(/* 返回值 */);
// 替换到容器中
swap(YourDependencyAction::class, $mockAction);
// 解析要测试的Action - 容器会注入Mock依赖
$result = resolve(ActionUnderTest::class)(/* 参数 */);Database Assertions
数据库断言
php
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\assertDatabaseCount;
assertDatabaseHas('orders', ['id' => $order->id]);
assertDatabaseCount('orders', 1);php
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\assertDatabaseCount;
assertDatabaseHas('orders', ['id' => $order->id]);
assertDatabaseCount('orders', 1);