Laravel 13 测试改进详解 摘要 Laravel 13 对测试系统进行了多项改进,包括更好的并行测试支持和测试隔离。本文将深入讲解 Laravel 13 的测试改进,包括:
并行测试配置与优化 测试隔离改进 Pest 与 PHPUnit 双支持 测试辅助方法增强 模拟与断言改进 实战案例:构建完整测试套件 本文适合希望提升测试效率的 Laravel 开发者。
1. 测试改进概览 1.1 主要改进 改进 描述 并行测试 更好的隔离与性能 Pest 支持 第一方 Pest 支持 测试辅助 新增便捷方法 断言增强 更多语义化断言
1.2 测试框架支持 Laravel 13 同时支持 PHPUnit 和 Pest:
1 2 3 4 5 php artisan test php artisan test --pest
2. 并行测试 2.1 启用并行测试 1 php artisan test --parallel
2.2 配置进程数 1 php artisan test --parallel --processes=4
2.3 配置文件 1 2 3 4 <php> <env name="TEST_PARALLEL_PROCESSES" value="4" /> </php>
2.4 数据库隔离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use Illuminate \Foundation \Testing \DatabaseMigrations ;use Illuminate \Foundation \Testing \RefreshDatabase ;abstract class TestCase extends BaseTestCase { use RefreshDatabase ; protected function parallelProcessId ( ): int { return (int ) env ('TEST_PARALLEL_PROCESS_ID' , 1 ); } protected function getDatabaseName ( ): string { return 'test_' . $this ->parallelProcessId (); } }
3. Pest 测试 3.1 安装 Pest 1 2 composer require pestphp/pest --dev php artisan pest:install
3.2 Pest 测试示例 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 use App \Models \User ;use function Pest \Laravel \{actingAs , get , post };it ('can list users' , function () { $users = User ::factory ()->count (10 )->create (); $response = get ('/api/users' ); $response ->assertStatus (200 ) ->assertJsonCount (10 , 'data' ); }); it ('requires authentication' , function () { get ('/api/users' )->assertUnauthorized (); }); it ('can create a user' , function () { $data = [ 'name' => 'John Doe' , 'email' => 'john@example.com' , 'password' => 'password' , ]; post ('/api/users' , $data ) ->assertCreated () ->assertJsonPath ('data.email' , 'john@example.com' ); });
3.3 Pest 数据集 1 2 3 4 5 6 7 8 9 10 11 dataset ('user_emails' , [ 'john@example.com' , 'jane@example.com' , 'bob@example.com' , ]); it ('validates email format' , function ($email ) { post ('/api/users' , ['email' => $email ]) ->assertValid ('email' ); })->with ('user_emails' );
4. 测试辅助方法 4.1 新增断言 1 2 3 4 5 6 7 8 9 10 11 $this ->assertDatabaseHasModel (User ::class , ['email' => 'john@example.com' ]);$this ->assertDatabaseMissingModel (User ::class , ['email' => 'nonexistent@example.com' ]);$this ->assertSoftDeleted ($user );$this ->assertNotSoftDeleted ($user );
4.2 便捷方法 1 2 3 4 5 6 7 8 9 10 11 12 13 $user = $this ->actingAsUser ();$admin = $this ->actingAsAdmin ();$response ->assertJsonStructure ([ 'data' => [ '*' => ['id' , 'name' , 'email' ], ], ]); $response ->assertJsonFragment (['name' => 'John' ]);
4.3 模型断言 1 2 3 4 5 6 7 8 $this ->assertModelEquals ($expected , $actual );$this ->assertModelCollectionEquals ( User ::whereIn ('id' , [1 , 2 , 3 ])->get (), $actualCollection );
5. 模拟增强 5.1 队列模拟 1 2 3 4 5 6 7 8 9 10 11 use Illuminate \Support \Facades \Queue ;use App \Jobs \ProcessPodcast ;Queue ::fake ();Queue ::assertPushed (ProcessPodcast ::class );Queue ::assertPushedOn ('podcasts' , ProcessPodcast ::class );Queue ::assertPushedTimes (ProcessPodcast ::class , 2 );Queue ::assertNotPushed (AnotherJob ::class );
5.2 事件模拟 1 2 3 4 5 6 7 8 9 10 use Illuminate \Support \Facades \Event ;use App \Events \UserRegistered ;Event ::fake ();Event ::assertDispatched (UserRegistered ::class );Event ::assertDispatchedTimes (UserRegistered ::class , 1 );Event ::assertNotDispatched (AnotherEvent ::class );
5.3 HTTP 模拟 1 2 3 4 5 6 7 8 9 10 11 use Illuminate \Support \Facades \Http ;Http ::fake ([ 'api.example.com/*' => Http ::response (['status' => 'ok' ], 200 ), ]); Http ::assertSent (function ($request ) { return $request ->url () === 'https://api.example.com/users' ; });
6. 测试隔离 6.1 数据库事务 1 2 3 4 5 6 7 8 use Illuminate \Foundation \Testing \DatabaseTransactions ;class UserTest extends TestCase { use DatabaseTransactions ; }
6.2 模型工厂 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use App \Models \User ;class UserTest extends TestCase { protected function setUp ( ): void { parent ::setUp (); } protected function tearDown ( ): void { parent ::tearDown (); } }
6.3 环境隔离 1 2 3 4 5 6 7 8 9 10 11 abstract class TestCase extends BaseTestCase { protected function setUp ( ): void { parent ::setUp (); config (['app.env' => 'testing' ]); } }
7. 实战案例 7.1 完整测试套件 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 <?php namespace Tests \Feature ;use App \Models \User ;use App \Models \Post ;use Illuminate \Foundation \Testing \RefreshDatabase ;use Tests \TestCase ;class PostTest extends TestCase { use RefreshDatabase ; protected User $user ; protected function setUp ( ): void { parent ::setUp (); $this ->user = User ::factory ()->create (); } public function test_guest_cannot_create_post ( ): void { $response = $this ->postJson ('/api/posts' , [ 'title' => 'Test Post' , 'content' => 'Test Content' , ]); $response ->assertUnauthorized (); } public function test_authenticated_user_can_create_post ( ): void { $response = $this ->actingAs ($this ->user) ->postJson ('/api/posts' , [ 'title' => 'Test Post' , 'content' => 'Test Content' , ]); $response ->assertCreated () ->assertJsonPath ('data.title' , 'Test Post' ) ->assertJsonPath ('data.user_id' , $this ->user->id); $this ->assertDatabaseHas ('posts' , [ 'title' => 'Test Post' , 'user_id' => $this ->user->id, ]); } public function test_user_can_list_own_posts ( ): void { $posts = Post ::factory ()->count (5 )->for ($this ->user)->create (); $response = $this ->actingAs ($this ->user) ->getJson ('/api/posts' ); $response ->assertOk () ->assertJsonCount (5 , 'data' ); } public function test_user_can_update_own_post ( ): void { $post = Post ::factory ()->for ($this ->user)->create (); $response = $this ->actingAs ($this ->user) ->putJson ("/api/posts/{$post->id} " , [ 'title' => 'Updated Title' , ]); $response ->assertOk () ->assertJsonPath ('data.title' , 'Updated Title' ); } public function test_user_cannot_update_others_post ( ): void { $otherUser = User ::factory ()->create (); $post = Post ::factory ()->for ($otherUser )->create (); $response = $this ->actingAs ($this ->user) ->putJson ("/api/posts/{$post->id} " , [ 'title' => 'Updated Title' , ]); $response ->assertForbidden (); } public function test_user_can_delete_own_post ( ): void { $post = Post ::factory ()->for ($this ->user)->create (); $response = $this ->actingAs ($this ->user) ->deleteJson ("/api/posts/{$post->id} " ); $response ->assertNoContent (); $this ->assertDatabaseMissing ('posts' , [ 'id' => $post ->id, ]); } }
7.2 Pest 版本 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 use App \Models \User ;use App \Models \Post ;use function Pest \Laravel \{actingAs , get , post , put , delete };beforeEach (function () { $this ->user = User ::factory ()->create (); }); it ('guest cannot create post' , function () { post ('/api/posts' , ['title' => 'Test' ]) ->assertUnauthorized (); }); it ('authenticated user can create post' , function () { actingAs ($this ->user) ->post ('/api/posts' , [ 'title' => 'Test Post' , 'content' => 'Test Content' , ]) ->assertCreated () ->assertJsonPath ('data.title' , 'Test Post' ); expect (Post ::count ())->toBe (1 ); }); it ('user can list own posts' , function () { Post ::factory ()->count (5 )->for ($this ->user)->create (); actingAs ($this ->user) ->get ('/api/posts' ) ->assertOk () ->assertJsonCount (5 , 'data' ); });
8. 最佳实践 8.1 测试命名 1 2 3 4 5 public function test_user_can_create_post_when_authenticated ( ): void {}public function testCreate ( ): void {}
8.2 测试组织 1 2 3 4 5 6 7 8 9 tests/ ├── Feature/ │ ├── Auth/ │ ├── Posts/ │ └── Users/ ├── Unit/ │ ├── Models/ │ └── Services/ └── TestCase.php
8.3 测试覆盖率 1 php artisan test --coverage
9. 总结 Laravel 13 的测试改进为开发者提供了更好的测试体验:
并行测试 :显著提升测试速度Pest 支持 :更优雅的测试语法增强断言 :更多语义化方法测试隔离 :更好的并行支持通过本指南,您已经掌握了 Laravel 13 测试改进的核心内容,可以构建更完善的测试套件了。
参考资料