Laravel 13 测试进阶指南
测试是保证代码质量的重要手段。Laravel 13 提供了丰富的测试工具和方法,本文将深入探讨 Laravel 13 的高级测试技巧。
测试环境配置
PHPUnit 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache" executionOrder="depends,defects" failOnRisky="true" failOnWarning="true"> <testsuites> <testsuite name="Unit"> <directory>tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory>tests/Feature</directory> </testsuite> </testsuites> <source> <include> <directory>app</directory> </include> </source> <php> <env name="APP_ENV" value="testing"/> <env name="BCRYPT_ROUNDS" value="4"/> <env name="CACHE_STORE" value="array"/> <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> <env name="MAIL_MAILER" value="array"/> <env name="QUEUE_CONNECTION" value="sync"/> <env name="SESSION_DRIVER" value="array"/> </php> </phpunit>
|
特性测试
基础测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace Tests\Feature;
use Tests\TestCase; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase { use RefreshDatabase;
public function test_user_can_be_created(): void { $response = $this->postJson('/api/users', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password123', ]);
$response->assertStatus(201) ->assertJson([ 'message' => 'User created successfully', ]);
$this->assertDatabaseHas('users', [ 'email' => 'john@example.com', ]); } }
|
认证测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <?php
namespace Tests\Feature;
use Tests\TestCase; use App\Models\User; use Laravel\Sanctum\Sanctum;
class AuthenticatedTest extends TestCase { public function test_authenticated_user_can_access_protected_route(): void { $user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/user');
$response->assertStatus(200) ->assertJson([ 'id' => $user->id, 'email' => $user->email, ]); }
public function test_unauthenticated_user_cannot_access_protected_route(): void { $response = $this->getJson('/api/user');
$response->assertStatus(401); } }
|
单元测试
服务测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php
namespace Tests\Unit\Services;
use Tests\TestCase; use App\Services\PaymentService; use App\Models\Order; use Mockery;
class PaymentServiceTest extends TestCase { protected function tearDown(): void { Mockery::close(); parent::tearDown(); }
public function test_process_payment_successfully(): void { $order = Order::factory()->make(['total' => 100.00]);
$gateway = Mockery::mock('App\Contracts\PaymentGateway'); $gateway->shouldReceive('charge') ->once() ->with($order->total, $order->id) ->andReturn(['status' => 'success', 'transaction_id' => 'txn_123']);
$service = new PaymentService($gateway);
$result = $service->process($order);
$this->assertEquals('success', $result['status']); $this->assertEquals('txn_123', $result['transaction_id']); } }
|
模型测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace Tests\Unit\Models;
use Tests\TestCase; use App\Models\User; use App\Models\Post; use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase { use RefreshDatabase;
public function test_user_has_many_posts(): void { $user = User::factory()->create(); $posts = Post::factory()->count(3)->create(['user_id' => $user->id]);
$this->assertCount(3, $user->posts); $this->assertInstanceOf(Post::class, $user->posts->first()); }
public function test_user_full_name_attribute(): void { $user = User::factory()->create([ 'first_name' => 'John', 'last_name' => 'Doe', ]);
$this->assertEquals('John Doe', $user->full_name); }
public function test_user_email_is_verified(): void { $user = User::factory()->create([ 'email_verified_at' => now(), ]);
$this->assertTrue($user->hasVerifiedEmail()); } }
|
HTTP 测试
请求测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public function test_get_users_list(): void { User::factory()->count(10)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200) ->assertJsonCount(10, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'name', 'email', 'created_at'] ], 'meta' => ['total', 'per_page', 'current_page'] ]); }
public function test_create_user_validation(): void { $response = $this->postJson('/api/users', [ 'name' => '', 'email' => 'invalid-email', 'password' => 'short', ]);
$response->assertStatus(422) ->assertJsonValidationErrors(['name', 'email', 'password']); }
public function test_update_user(): void { $user = User::factory()->create();
$response = $this->putJson("/api/users/{$user->id}", [ 'name' => 'Updated Name', ]);
$response->assertStatus(200);
$this->assertDatabaseHas('users', [ 'id' => $user->id, 'name' => 'Updated Name', ]); }
public function test_delete_user(): void { $user = User::factory()->create();
$response = $this->deleteJson("/api/users/{$user->id}");
$response->assertStatus(204);
$this->assertDatabaseMissing('users', [ 'id' => $user->id, ]); }
|
数据库测试
数据库断言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public function test_database_assertions(): void { $user = User::factory()->create();
$this->assertDatabaseHas('users', [ 'email' => $user->email, ]);
$this->assertDatabaseCount('users', 1);
$this->assertDatabaseMissing('users', [ 'email' => 'nonexistent@example.com', ]);
$user->delete();
$this->assertSoftDeleted('users', [ 'id' => $user->id, ]); }
|
数据工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?php
namespace Database\Factories;
use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash;
class UserFactory extends Factory { protected $model = User::class;
public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'password' => Hash::make('password'), 'email_verified_at' => now(), ]; }
public function unverified(): static { return $this->state(fn (array $attributes) => [ 'email_verified_at' => null, ]); }
public function admin(): static { return $this->state(fn (array $attributes) => [ 'role' => 'admin', ]); } }
$user = User::factory()->create(); $users = User::factory()->count(10)->create(); $admin = User::factory()->admin()->create(); $unverified = User::factory()->unverified()->create();
|
模拟和存根
模拟门面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <?php
namespace Tests\Feature;
use Tests\TestCase; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Mail; use App\Jobs\ProcessPodcast; use App\Mail\WelcomeEmail;
class MockTest extends TestCase { public function test_job_is_dispatched(): void { Queue::fake();
$this->postJson('/api/podcasts', [ 'title' => 'Test Podcast', 'url' => 'https://example.com/podcast.mp3', ]);
Queue::assertPushed(ProcessPodcast::class); Queue::assertPushedOn('podcasts', ProcessPodcast::class); Queue::assertPushed(function (ProcessPodcast $job) { return $job->podcast->title === 'Test Podcast'; }); }
public function test_mail_is_sent(): void { Mail::fake();
$user = User::factory()->create();
$this->postJson('/api/register', [ 'name' => $user->name, 'email' => $user->email, 'password' => 'password', ]);
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) { return $mail->hasTo($user->email); }); }
public function test_event_is_dispatched(): void { Event::fake();
$order = Order::factory()->create(); $this->postJson("/api/orders/{$order->id}/ship");
Event::assertDispatched(OrderShipped::class); Event::assertDispatched(function (OrderShipped $event) use ($order) { return $event->order->id === $order->id; }); } }
|
模拟服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function test_with_mocked_service(): void { $this->mock(PaymentService::class, function ($mock) { $mock->shouldReceive('process') ->once() ->andReturn(['status' => 'success']); });
$response = $this->postJson('/api/orders', [ 'amount' => 100, ]);
$response->assertStatus(201); }
|
测试数据
测试数据提供者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?php
namespace Tests\Feature;
use Tests\TestCase;
class UserValidationTest extends TestCase { public static function invalidEmailProvider(): array { return [ 'empty email' => [''], 'invalid format' => ['invalid-email'], 'missing domain' => ['user@'], 'missing tld' => ['user@example'], ]; }
#[\PHPUnit\Framework\Attributes\DataProvider('invalidEmailProvider')] public function test_invalid_email_is_rejected(string $email): void { $response = $this->postJson('/api/users', [ 'name' => 'John Doe', 'email' => $email, 'password' => 'password123', ]);
$response->assertStatus(422) ->assertJsonValidationErrors(['email']); } }
|
测试中间件
测试认证中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public function test_authentication_required(): void { $response = $this->getJson('/api/user');
$response->assertStatus(401); }
public function test_authenticated_request(): void { $user = User::factory()->create();
$response = $this->actingAs($user) ->getJson('/api/user');
$response->assertStatus(200); }
|
测试授权中间件
1 2 3 4 5 6 7 8 9
| public function test_user_cannot_access_admin_routes(): void { $user = User::factory()->create(['role' => 'user']);
$response = $this->actingAs($user) ->getJson('/api/admin/users');
$response->assertStatus(403); }
|
测试命令
测试 Artisan 命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
namespace Tests\Feature\Console;
use Tests\TestCase; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase;
class SendEmailsCommandTest extends TestCase { use RefreshDatabase;
public function test_command_sends_emails(): void { User::factory()->count(5)->create();
Mail::fake();
$this->artisan('emails:send') ->expectsOutput('Sending emails...') ->assertExitCode(0);
Mail::assertSent(NewsletterEmail::class, 5); } }
|
测试最佳实践
1. 测试命名规范
1 2 3 4 5 6 7 8 9
| public function test_user_can_create_post(): void {} public function test_user_cannot_delete_others_post(): void {} public function test_email_must_be_valid(): void {}
public function testCreate(): void {} public function testDelete(): void {} public function testEmail(): void {}
|
2. 测试结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public function test_user_can_create_post(): void { $user = User::factory()->create(); $postData = [ 'title' => 'Test Post', 'content' => 'Test content', ];
$response = $this->actingAs($user) ->postJson('/api/posts', $postData);
$response->assertStatus(201); $this->assertDatabaseHas('posts', $postData); }
|
3. 使用测试辅助方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| protected function actingAsUser(?User $user = null): User { $user = $user ?? User::factory()->create(); $this->actingAs($user); return $user; }
protected function createPost(array $attributes = []): Post { return Post::factory()->create($attributes); }
public function test_user_can_view_own_post(): void { $user = $this->actingAsUser(); $post = $this->createPost(['user_id' => $user->id]);
$response = $this->getJson("/api/posts/{$post->id}");
$response->assertStatus(200); }
|
运行测试
命令行选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| php artisan test
php artisan test tests/Feature/UserTest.php
php artisan test --filter test_user_can_create_post
php artisan test --parallel
php artisan test --coverage
php artisan test --rerun-filter
|
总结
Laravel 13 提供了强大而全面的测试支持。通过合理使用特性测试、单元测试、模拟和存根等工具,可以构建出高质量的测试套件。记住遵循 AAA 模式(Arrange、Act、Assert),使用有意义的测试命名,并保持测试的独立性和可重复性。良好的测试覆盖率是代码质量的保障,也是重构的信心来源。