Laravel 12 测试体系:Pest v3 集成与测试策略 摘要 本文详解 Laravel 12 与 Pest v3 的深度集成,包括测试用例的编写、测试套件的组织、Mock 与 Stub 的使用。提供完整的测试策略,覆盖单元测试、集成测试、功能测试和端到端测试,帮助开发者构建高质量、可维护的 Laravel 应用。
1. Laravel 测试体系概述 Laravel 12 提供了全面的测试支持,从单元测试到端到端测试,构建了一个完整的测试生态系统。
1.1 核心测试功能 PHPUnit 集成 :内置 PHPUnit 支持Pest v3 集成 :官方推荐的测试框架测试辅助函数 :丰富的测试辅助方法测试数据库 :支持 SQLite 内存数据库浏览器测试 :使用 Laravel Dusk 进行端到端测试API 测试 :专门的 API 测试辅助函数Mock 与 Stub :内置的模拟功能测试覆盖率 :支持代码覆盖率分析1.2 测试类型与应用场景 测试类型 描述 适用场景 工具 单元测试 测试单个类或方法 业务逻辑、工具类 Pest/PHPUnit 集成测试 测试多个组件的交互 控制器、服务 Pest/PHPUnit 功能测试 测试完整的用户流程 注册、登录、购物车 Pest/PHPUnit API 测试 测试 API 端点 RESTful API、GraphQL Pest/PHPUnit 浏览器测试 测试浏览器交互 前端交互、JavaScript Laravel Dusk
2. Pest v3 集成 Laravel 12 官方推荐使用 Pest v3 作为测试框架,提供了更简洁、更优雅的测试语法。
2.1 安装与配置 安装 Pest v3 1 2 3 4 5 composer require pestphp/pest --dev composer require pestphp/pest-plugin-laravel --dev ./vendor/bin/pest --init
配置 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 use Illuminate \Foundation \Testing \RefreshDatabase ;use Illuminate \Foundation \Testing \WithFaker ;uses (Tests\TestCase ::class , RefreshDatabase ::class , WithFaker ::class )->in ('Feature' );uses (Tests\TestCase ::class )->in ('Unit' );expect ()->extend ('toBeOne' , function () { return $this ->toBe (1 ); }); function actingAsAdmin ( ) { return test ()->actingAs ( App\Models\User ::factory ()->create (['role' => 'admin' ]) ); }
2.2 Pest v3 语法特性 基本测试结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 test ('basic test' , function () { expect (true )->toBeTrue (); }); test ('user can register' , function () { })->describe ('User Registration' ); test ('addition works' , function ($a , $b , $expected ) { expect ($a + $b )->toBe ($expected ); })->with ([ [1 , 2 , 3 ], [2 , 3 , 5 ], [3 , 4 , 7 ], ]);
测试生命周期钩子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 beforeEach (function () { $this ->user = User ::factory ()->create (); }); afterEach (function () { }); beforeAll (function () { }); afterAll (function () { });
测试分组 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 describe ('User Management' , function () { test ('user can be created' , function () { }); test ('user can be updated' , function () { }); test ('user can be deleted' , function () { }); }); describe ('Authentication' , function () { describe ('Login' , function () { test ('user can login with valid credentials' , function () { }); test ('user cannot login with invalid credentials' , function () { }); }); describe ('Registration' , function () { test ('user can register with valid data' , function () { }); test ('user cannot register with invalid data' , function () { }); }); });
3. 单元测试 单元测试是测试的基础,用于测试单个类或方法的功能。
3.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 describe ('UserService' , function () { test ('can create user' , function () { $userService = new UserService (); $userData = [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'password' => 'password123' , ]; $user = $userService ->create ($userData ); expect ($user )->toBeInstanceOf (User ::class ); expect ($user ->name)->toBe ('Test User' ); expect ($user ->email)->toBe ('test@example.com' ); }); test ('can find user by email' , function () { $userService = new UserService (); $user = User ::factory ()->create (['email' => 'test@example.com' ]); $foundUser = $userService ->findByEmail ('test@example.com' ); expect ($foundUser )->toBeInstanceOf (User ::class ); expect ($foundUser ->id)->toBe ($user ->id); }); });
测试工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 describe ('StrHelper' , function () { test ('can generate slug' , function () { $slug = StrHelper ::slug ('Hello World' ); expect ($slug )->toBe ('hello-world' ); }); test ('can generate random string' , function () { $random = StrHelper ::random (10 ); expect ($random )->toHaveLength (10 ); }); });
3.2 Mock 与 Stub 使用 Mock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 test ('user service uses mailer to send welcome email' , function () { $mailer = Mockery ::mock (Mailer ::class ); $mailer ->shouldReceive ('sendWelcomeEmail' ) ->once () ->with (Mockery ::type (User ::class )); $userService = new UserService ($mailer ); $userData = [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'password' => 'password123' , ]; $user = $userService ->create ($userData ); expect ($user )->toBeInstanceOf (User ::class ); });
使用 Laravel 的 Mock 辅助函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 test ('user service uses mailer to send welcome email' , function () { $this ->mock (Mailer ::class , function ($mock ) { $mock ->shouldReceive ('sendWelcomeEmail' ) ->once () ->with ($this ->callback (function ($user ) { return $user instanceof User; })); }); $userService = app (UserService ::class ); $userData = [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'password' => 'password123' , ]; $user = $userService ->create ($userData ); expect ($user )->toBeInstanceOf (User ::class ); });
4. 集成测试 集成测试用于测试多个组件的交互,如控制器与服务的协作。
4.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 describe ('UserController' , function () { test ('can get user profile' , function () { $user = User ::factory ()->create (); $response = $this ->actingAs ($user )->get ('/profile' ); $response ->assertStatus (200 ); $response ->assertViewIs ('profile' ); $response ->assertViewHas ('user' , $user ); }); test ('can update user profile' , function () { $user = User ::factory ()->create (); $data = [ 'name' => 'Updated Name' , 'email' => 'updated@example.com' , ]; $response = $this ->actingAs ($user )->put ('/profile' , $data ); $response ->assertRedirect ('/profile' ); $this ->assertDatabaseHas ('users' , [ 'id' => $user ->id, 'name' => 'Updated Name' , 'email' => 'updated@example.com' , ]); }); });
表单验证测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 test ('cannot update profile with invalid data' , function () { $user = User ::factory ()->create (); $data = [ 'name' => '' , 'email' => 'invalid-email' , ]; $response = $this ->actingAs ($user )->put ('/profile' , $data ); $response ->assertStatus (302 ); $response ->assertSessionHasErrors (['name' , 'email' ]); $this ->assertDatabaseMissing ('users' , [ 'id' => $user ->id, 'email' => 'invalid-email' , ]); });
4.2 测试服务 测试服务与数据库交互 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 test ('user service creates user in database' , function () { $userService = app (UserService ::class ); $userData = [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'password' => 'password123' , ]; $user = $userService ->create ($userData ); expect ($user )->toBeInstanceOf (User ::class ); $this ->assertDatabaseHas ('users' , [ 'email' => 'test@example.com' , ]); });
测试服务与外部 API 交互 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 test ('payment service processes payment' , function () { $this ->mock (PaymentGateway ::class , function ($mock ) { $mock ->shouldReceive ('charge' ) ->once () ->with (100 , 'card_123' ) ->andReturn (['success' => true , 'transaction_id' => 'tx_123' ]); }); $paymentService = app (PaymentService ::class ); $user = User ::factory ()->create (); $result = $paymentService ->process ($user , 100 , 'card_123' ); expect ($result )->toBeTrue (); $this ->assertDatabaseHas ('payments' , [ 'user_id' => $user ->id, 'amount' => 100 , 'transaction_id' => 'tx_123' , ]); });
5. API 测试 Laravel 12 提供了专门的 API 测试辅助函数,简化了 API 测试的编写。
5.1 基本 API 测试 测试 GET 请求 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 describe ('User API' , function () { test ('can get users list' , function () { User ::factory ()->count (3 )->create (); $user = User ::factory ()->create (['role' => 'admin' ]); $response = $this ->actingAs ($user )->getJson ('/api/users' ); $response ->assertStatus (200 ); $response ->assertJsonCount (4 , 'data' ); }); test ('can get single user' , function () { $user = User ::factory ()->create (); $admin = User ::factory ()->create (['role' => 'admin' ]); $response = $this ->actingAs ($admin )->getJson ("/api/users/{$user->id} " ); $response ->assertStatus (200 ); $response ->assertJson ([ 'data' => [ 'id' => $user ->id, 'name' => $user ->name, 'email' => $user ->email, ], ]); }); });
测试 POST 请求 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 test ('can create user' , function () { $admin = User ::factory ()->create (['role' => 'admin' ]); $userData = [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'password' => 'password123' , 'role' => 'user' , ]; $response = $this ->actingAs ($admin )->postJson ('/api/users' , $userData ); $response ->assertStatus (201 ); $response ->assertJson ([ 'data' => [ 'name' => 'Test User' , 'email' => 'test@example.com' , 'role' => 'user' , ], ]); $this ->assertDatabaseHas ('users' , [ 'email' => 'test@example.com' , ]); });
5.2 API 认证测试 测试认证中间件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 test ('unauthorized user cannot access protected API' , function () { $response = $this ->getJson ('/api/users' ); $response ->assertStatus (401 ); $response ->assertJson ([ 'message' => 'Unauthenticated.' , ]); });
测试 API 速率限制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 test ('API has rate limit' , function () { $user = User ::factory ()->create (); for ($i = 0 ; $i < 60 ; $i ++) { $response = $this ->actingAs ($user )->getJson ('/api/users' ); } $response ->assertStatus (429 ); $response ->assertJson ([ 'message' => 'Too Many Attempts.' , ]); });
6. 浏览器测试 Laravel Dusk 提供了强大的浏览器测试功能,可以测试前端交互和 JavaScript 功能。
6.1 安装与配置 安装 Laravel Dusk 1 2 3 4 composer require laravel/dusk --dev php artisan dusk:install
配置 Dusk 1 2 3 4 5 6 7 APP_URL=http: APP_ENV=testing APP_DEBUG=true DB_CONNECTION=sqlite DB_DATABASE=:memory:
6.2 编写浏览器测试 测试登录流程 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 namespace Tests \Browser ;use Laravel \Dusk \Browser ;use Tests \DuskTestCase ;class LoginTest extends DuskTestCase { test ('user can login' , function () { $user = User ::factory ()->create ([ 'email' => 'test@example.com' , 'password' => bcrypt ('password123' ), ]); $this ->browse (function (Browser $browser ) use ($user ) { $browser ->visit ('/login ') ->type ('email ', $user ->email ) ->type ('password ', 'password123 ') ->press ('Login ') ->assertPathIs ('/dashboard ') ->assertSee ('Dashboard '); }); }); test ('user cannot login with invalid credentials' , function () { $this ->browse (function (Browser $browser ) { $browser ->visit ('/login' ) ->type ('email' , 'invalid@example.com' ) ->type ('password' , 'wrong-password' ) ->press ('Login' ) ->assertPathIs ('/login' ) ->assertSee ('These credentials do not match our records.' ); }); }); }
测试表单提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 test ('user can register' , function () { $this ->browse (function (Browser $browser ) { $browser ->visit ('/register' ) ->type ('name' , 'Test User' ) ->type ('email' , 'test@example.com' ) ->type ('password' , 'password123' ) ->type ('password_confirmation' , 'password123' ) ->press ('Register' ) ->assertPathIs ('/dashboard' ) ->assertSee ('Dashboard' ); }); $this ->assertDatabaseHas ('users' , [ 'email' => 'test@example.com' , ]); });
7. 测试套件的组织 7.1 目录结构 1 2 3 4 5 6 7 8 9 10 11 12 tests/ ├── Unit/ # 单元测试 │ ├── Services/ # 服务测试 │ ├── Helpers/ # 辅助函数测试 │ └── Models/ # 模型测试 ├── Feature/ # 集成测试 │ ├── Controllers/ # 控制器测试 │ ├── Api/ # API 测试 │ └── Pages/ # 页面测试 ├── Browser/ # 浏览器测试 ├── Pest.php # Pest 配置 └── TestCase.php # 基础测试类
7.2 测试分类 按功能分类 1 2 3 4 5 6 tests/ ├── Feature/ │ ├── Authentication/ # 认证相关测试 │ ├── UserManagement/ # 用户管理测试 │ ├── Payment/ # 支付相关测试 │ └── Product/ # 产品相关测试
按模块分类 1 2 3 4 5 tests/ ├── Feature/ │ ├── Admin/ # 管理员功能测试 │ ├── User/ # 用户功能测试 │ └── Guest/ # 访客功能测试
8. 测试覆盖率分析 8.1 配置代码覆盖率 安装 Xdebug 1 2 3 4 5 sudo apt-get install php-xdebugpecl install xdebug
配置 PHPUnit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <phpunit xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation ="vendor/phpunit/phpunit/phpunit.xsd" bootstrap ="vendor/autoload.php" colors ="true" > <coverage processUncoveredFiles ="true" > <include > <directory suffix =".php" > ./app</directory > </include > <exclude > <directory suffix =".php" > ./app/Http/Controllers</directory > <directory suffix =".php" > ./app/Models</directory > </exclude > </coverage > </phpunit >
8.2 生成覆盖率报告 使用 Pest 生成覆盖率报告 1 2 3 4 5 6 7 8 ./vendor/bin/pest --coverage-html=coverage ./vendor/bin/pest --coverage-clover=coverage.xml ./vendor/bin/pest --coverage-text
分析覆盖率报告 覆盖率报告通常包括以下指标:
行覆盖率 :测试覆盖的代码行数百分比分支覆盖率 :测试覆盖的代码分支百分比路径覆盖率 :测试覆盖的代码路径百分比函数覆盖率 :测试覆盖的函数百分比8.3 提高测试覆盖率 识别未覆盖的代码 1 2 ./vendor/bin/pest --coverage-text --coverage-show-uncovered
优先测试核心功能 业务逻辑 :优先测试核心业务逻辑边界情况 :测试边界条件和异常情况关键路径 :测试用户常用的功能路径易错代码 :测试历史上容易出错的代码9. CI/CD 中的测试自动化 9.1 GitHub Actions 配置 基本配置 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 name: Run Tests on: push: branches: [ main , develop ] pull_request: branches: [ main , develop ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' extensions: mbstring, dom, curl, sqlite3 coverage: xdebug - name: Install dependencies run: composer install --prefer-dist --no-progress - name: Run tests run: ./vendor/bin/pest --coverage-clover=coverage.xml - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml
多环境测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 jobs: test: runs-on: ubuntu-latest strategy: matrix: php: [8.1 , 8.2 , 8.3 ] laravel: [10 .* , 11 .* , 12 .* ] steps: - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" --no-update composer install --prefer-dist --no-progress - name: Run tests run: ./vendor/bin/pest
9.2 GitLab CI 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 stages: - test run_tests: stage: test image: php:8.2 script: - apt-get update && apt-get install -y git unzip - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - composer install --prefer-dist --no-progress - ./vendor/bin/pest artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml
10. 测试最佳实践 10.1 编写高质量测试 测试名称 :使用描述性的测试名称测试结构 :遵循 Arrange-Act-Assert 模式测试隔离 :每个测试应该独立运行测试速度 :保持测试快速运行测试维护 :保持测试代码的整洁和可维护10.2 测试原则 FIRST 原则 :
Fast :测试应该快速运行Independent :测试应该相互独立Repeatable :测试应该可重复运行Self-validating :测试应该自动验证结果Timely :测试应该及时编写测试金字塔 :
底部 :大量的单元测试中间 :适量的集成测试顶部 :少量的端到端测试10.3 常见测试陷阱 测试实现细节 :避免测试实现细节,应该测试行为过度模拟 :不要过度使用 Mock,应该测试真实的交互测试环境依赖 :避免测试依赖外部服务测试速度慢 :避免测试中使用真实的数据库和网络请求测试覆盖率迷恋 :不要只追求覆盖率,应该关注测试质量11. 实战案例:构建完整的测试套件 11.1 项目背景 规模 :中型 e-commerce 应用功能 :产品管理、用户管理、购物车、支付要求 :测试覆盖率 > 80%11.2 测试策略设计 1. 测试分层 单元测试 :70%
集成测试 :20%
端到端测试 :10%
2. 测试重点 核心业务逻辑 :产品管理、购物车、支付用户流程 :注册、登录、购买API 接口 :RESTful API边界情况 :库存不足、支付失败3. 测试实现 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 describe ('ProductService' , function () { test ('can create product' , function () { }); test ('can update product' , function () { }); test ('can delete product' , function () { }); test ('can get product by id' , function () { }); test ('can search products' , function () { }); }); describe ('CartService' , function () { test ('can add product to cart' , function () { }); test ('can remove product from cart' , function () { }); test ('can update cart item quantity' , function () { }); test ('can calculate cart total' , function () { }); }); describe ('PaymentService' , function () { test ('can process payment' , function () { }); test ('can refund payment' , function () { }); test ('handles payment failure' , function () { }); });
11.3 测试效果 指标 实现前 实现后 提升 测试覆盖率 0% 85% 85% 代码质量 中等 高 显著 缺陷率 10% 2% 80% 开发速度 慢 快 显著 维护成本 高 低 显著
12. 总结 Laravel 12 的测试体系提供了从单元测试到端到端测试的完整支持,结合 Pest v3 的优雅语法,为开发者构建高质量应用提供了强大的工具。
通过本文的介绍,开发者可以快速掌握 Laravel 12 的测试能力,构建一个完整、高效的测试套件。测试不仅可以保证代码质量,还可以提高开发效率,减少生产环境的 bug。
在实际项目中,应该根据项目的规模和复杂度,选择合适的测试策略,平衡测试的深度和广度。同时,应该将测试作为开发流程的一部分,持续编写和维护测试代码。
随着 Laravel 生态系统的不断发展,测试工具和实践也在不断演进。开发者应该保持关注 Laravel 的更新,及时采用新的测试技术和工具,为构建更好的 Laravel 应用而努力。