资讯动态

ECC 实战指南:基于 Laravel 的 TDD 测试工作流(PHPUnit + Pest 全栈覆盖)

发布时间:2026/9/11 12:30:22 来源:尧图企业网站定制
ECC 实战指南基于 Laravel 的 TDD 测试工作流PHPUnit Pest 全栈覆盖【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC本篇技术指南以 ECC 开源仓库中的laravel-tdd技能文档docs/es/skills/laravel-tdd/SKILL.md 与 skills/laravel-tdd/SKILL.md为核心骨架系统讲解如何在 Laravel 应用中落地测试驱动开发从红-绿-重构循环、Unit/Feature/Integration 三层测试组织到RefreshDatabase等数据库策略、Model Factory、各类 Fakes 与 80% 覆盖率目标。读完本文你将掌握一套可直接复制到 Laravel 项目的完整测试方案并理解 ECC 如何通过tdd-workflow、laravel-verification等配套技能把测试闭环嵌入 CI 与合入前的验证流程。何时启用 laravel-tdd根据技能文档的定位以下场景应优先启用该技能在 Laravel 中新增功能或 API 端点修复 Bug 或进行重构测试 Eloquent 模型、policies、jobs 与 notifications为 Laravel 项目搭建测试基础设施编写 HTTP 控制器与 Form Request 的 feature 测试Mock 外部服务队列、邮件、通知、HTTP。ECC 的技术栈映射config/project-stack-mappings.json中只要项目存在composer.json、artisan或composer.lock任一指示文件就会被识别为php-laravel栈自动关联laravel-patterns、laravel-tdd、laravel-verification、laravel-security与tdd-workflow、verification-loop等技能并把测试命令预设为php artisan test、vendor/bin/phpunit、vendor/bin/pest。核心方法论红-绿-重构循环TDD 的引擎是三步循环写一个失败的测试RED——先定义期望行为此时实现尚未存在做最小改动让测试通过GREEN——只为实现目标行为写最少代码重构REFACTOR——在测试保持绿色前提下消除重复、改善命名与结构。// Step 1: RED — 编写一个会失败的测试 public function test_a_product_can_be_created(): void { $product Product::factory()-create([name Test Product]); $this-assertDatabaseHas(products, [name Test Product]); } // Step 2: GREEN — 编写 migration、model 与 factory // Step 3: REFACTOR — 在测试保持绿色的前提下改进代码ECC 的 skills/tdd-workflow/SKILL.md 进一步把这条循环固化为可验证的工程纪律RED 状态必须真实可复现测试被实际编译并执行、且因预期原因失败未确认 RED 之前不得改动生产代码每个阶段在 Git 仓库中对应一个 checkpoint 提交test: add reproducer for feature or bug→fix: feature or bug→refactor: clean up ...并在最后输出一份 TDD 证据报告记录计划任务、测试目标、RED/GREEN 证据与覆盖率结论。测试分层与选择策略技能文档将 Laravel 测试划分为三层按被测范围选择Unit 测试纯 PHP 类、值对象、服务层逻辑不依赖框架容器与数据库Feature 测试HTTP 端点、认证、校验、policies 与响应形态Integration 测试数据库 队列 外部服务边界一起验证。选择原则纯业务逻辑与服务用UnitHTTP、认证、校验、响应结构用Feature需要同时验证数据库/队列/外部服务的场景用Integration。这与 rules/php/testing.md 的建议一致——把快速单元测试与框架/数据库集成测试分离HTTP/控制器测试聚焦传输与校验业务规则下沉到服务层测试。数据库测试策略技能文档给出了三套数据库策略按场景取舍RefreshDatabase大多数 feature/integration 测试的默认选择。在支持事务的数据库上迁移只执行一次通过静态标志随后每条测试被事务包裹、结束后回滚对 SQLite:memory:或不支持事务的连接则每条测试前重新迁移。DatabaseTransactions当 schema 已迁移完毕、仅需按测试回滚时使用开销更小。DatabaseMigrations当每条测试都需要完整的migrate:fresh且能承受该成本时使用。测试隔离与确定性是底线优先用assertDatabaseHas断言数据库状态而不是手写查询。use App\Models\Project; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; final class ProjectRepositoryTest extends TestCase { use RefreshDatabase; public function test_project_can_be_retrieved_by_slug(): void { $project Project::factory()-create([slug alpha]); $found Project::query()-where(slug, alpha)-firstOrFail(); $this-assertSame($project-id, $found-id); } }测试框架选择Pest 优先PHPUnit 兼容文档的明确立场新测试默认使用 Pest仅当项目已标准化在 PHPUnit 或依赖 PHPUnit 专属工具时才继续使用 PHPUnit。两种框架在 ECC 的技能文档中都有完整示例。PHPUnit 示例控制器层use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; final class ProjectControllerTest extends TestCase { use RefreshDatabase; public function test_owner_can_create_project(): void { $user User::factory()-create(); $response $this-actingAs($user)-postJson(/api/projects, [ name New Project, ]); $response-assertCreated(); $this-assertDatabaseHas(projects, [name New Project]); } }Feature 测试示例HTTP 层use App\Models\Project; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; final class ProjectIndexTest extends TestCase { use RefreshDatabase; public function test_projects_index_returns_paginated_results(): void { $user User::factory()-create(); Project::factory()-count(3)-for($user)-create(); $response $this-actingAs($user)-getJson(/api/projects); $response-assertOk(); $response-assertJsonStructure([success, data, error, meta]); } }Pest 示例use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use function Pest\Laravel\actingAs; use function Pest\Laravel\assertDatabaseHas; uses(RefreshDatabase::class); test(owner can create project, function () { $user User::factory()-create(); $response actingAs($user)-postJson(/api/projects, [ name New Project, ]); $response-assertCreated(); assertDatabaseHas(projects, [name New Project]); });Pest 还可以用beforeEach集中准备数据用it()写出更接近自然语言的用例描述uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); beforeEach(function () { $this-user User::factory()-create(); $this-actingAs($this-user); }); it(creates a product with valid data, function () { $this-post(route(products.store), [ name Test Product, price 1999, ])-assertRedirect(); $this-assertDatabaseHas(products, [name Test Product]); });Model Factories 与状态用 Factory 生成测试数据为边界场景定义状态如 archived、admin、trial。技能文档强调用工厂而非手工create()调用并给出了带状态与关系的完整工厂示例// database/factories/UserFactory.php class UserFactory extends Factory { protected static ?string $password null; public function definition(): array { return [ name fake()-name(), email fake()-unique()-safeEmail(), email_verified_at now(), password static::$password ?? Hash::make(password), remember_token Str::random(10), role user, ]; } public function admin(): static { return $this-state(fn (array $attributes) [role admin]); } public function unverified(): static { return $this-state(fn (array $attributes) [email_verified_at null]); } }工厂的典型用法矩阵$user User::factory()-create(); $admin User::factory()-admin()-create(); $product Product::factory()-create([user_id $user-id]); $products Product::factory()-count(10)-create(); $draft Product::factory()-make(); // 仅构造不持久化 // 关联关系 $user User::factory()-has(Product::factory()-count(3))-create(); // 序列为多条记录依次应用不同状态 User::factory()-count(3)-sequence( [role admin], [role editor], [role user], )-create();模型测试scope、隐藏属性与关联模型的测试焦点是隐藏敏感属性、scope 过滤与关联关系class UserTest extends TestCase { use RefreshDatabase; public function test_it_hides_sensitive_attributes(): void { $user User::factory()-create(); $this-assertArrayNotHasKey(password, $user-toArray()); } public function test_admin_scope_returns_only_admins(): void { User::factory()-admin()-create(); User::factory()-count(3)-create(); $this-assertCount(1, User::admin()-get()); } }Feature / HTTP 测试认证、校验与授权边界Feature 层覆盖路由访问控制、表单校验和资源归属权public function test_guests_are_redirected_to_login(): void { $this-get(route(products.create))-assertRedirect(route(login)); } public function test_it_validates_required_fields(): void { $this-actingAs(User::factory()-create()); $this-post(route(products.store), []) -assertSessionHasErrors([name, price]); } public function test_users_cannot_modify_others_products(): void { $owner User::factory()-create(); $attacker User::factory()-create(); $product Product::factory()-create([user_id $owner-id]); $this-actingAs($attacker) -delete(route(products.destroy, $product)) -assertForbidden(); }JSON API 场景使用getJson/postJson配合assertUnauthorized、assertJsonCount、assertJsonStructure、assertJsonPath等断言public function test_unauthenticated_requests_are_rejected(): void { $this-getJson(/api/products)-assertUnauthorized(); } public function test_it_lists_paginated_products(): void { $user User::factory()-create(); Product::factory()-count(5)-create([user_id $user-id]); $response $this-actingAs($user)-getJson(/api/products); $response-assertOk(); $response-assertJsonCount(5, data); $response-assertJsonStructure([ data [[id, name, price]], meta [current_page, last_page, total], ]); }Sanctum 认证测试API 认证测试围绕注册、登录、错误口令与 token 鉴权展开。技能文档同时给出两套方案直接使用Sanctum::actingAs($user)模拟已认证用户use Laravel\Sanctum\Sanctum; Sanctum::actingAs($user); $response $this-getJson(/api/projects); $response-assertOk();或完整测试真实注册/登录/鉴权链路public function test_token_bearer_authenticates_requests(): void { $user User::factory()-create(); $token $user-createToken(test)-plainTextToken; $this-withToken($token) -getJson(/api/user) -assertOk() -assertJsonPath(data.email, $user-email); }Mock 与 Fakes隔离副作用技能文档系统梳理了 Laravel 内置 Fakes 的使用场景Fake用途Bus::fake()拦截 jobs 分发Queue::fake()拦截队列任务Mail::fake()捕获发出的邮件Notification::fake()捕获通知Event::fake()捕获领域事件Storage::fake(public)捕获文件系统操作Http::fake()Mock 外部 HTTP APIuse Illuminate\Support\Facades\Queue; Queue::fake(); dispatch(new SendOrderConfirmation($order-id)); Queue::assertPushed(SendOrderConfirmation::class);use Illuminate\Support\Facades\Notification; Notification::fake(); $user-notify(new InvoiceReady($invoice)); Notification::assertSentTo($user, InvoiceReady::class);Http::fake()支持按 URL 模式返回固定响应、错误响应或序列化场景如超时后重试成功Http::fake([ api.stripe.com/* Http::response([id pi_123, status succeeded], 200), ]); $result (new PaymentService())-charge(2999); $this-assertTrue($result-success); // 序列第一次 408第二次 200 —— 验证重试逻辑 Http::fake([ api.stripe.com/* Http::sequence() -pushStatus(408) -pushStatus(200), ]);文件上传场景配合Storage::fake与UploadedFile::fake()-image()Storage::fake(public); $file UploadedFile::fake()-image(photo.jpg, 200, 200); $response $this-actingAs($user)-post(/avatar, [avatar $file]); $response-assertSessionHasNoErrors(); Storage::disk(public)-assertExists(avatars/ . $file-hashName());授权测试Gate 与 policy 边界授权断言既可以在 Gate 层面直接验证也可以在 HTTP 层验证 403use Illuminate\Support\Facades\Gate; $this-assertTrue(Gate::forUser($user)-allows(update, $project)); $this-assertFalse(Gate::forUser($otherUser)-allows(update, $project));public function test_users_cannot_update_others_posts(): void { $post Post::factory()-create(); $this-actingAs(User::factory()-create()) -put(route(posts.update, $post), [title Hacked]) -assertForbidden(); }Inertia.js 响应测试使用 Inertia.js 的项目应使用assertInertia配合AssertableInertia校验组件名与 props而不是脆弱的原始 JSON 断言该原则同时被 rules/php/testing.md 收录use Inertia\Testing\AssertableInertia; $response $this-actingAs($user)-get(/dashboard); $response-assertOk(); $response-assertInertia(fn (AssertableInertia $page) $page -component(Dashboard) -where(user.id, $user-id) -has(projects) );Artisan 命令测试通过$this-artisan(...)测试自定义命令的输出与退出码配合 Fake 验证副作用public function test_it_sends_newsletters(): void { Mail::fake(); User::factory()-count(5)-create([subscribed true]); $this-artisan(newsletter:send) -expectsOutput(Sending newsletter to 5 subscribers...) -assertExitCode(0); Mail::assertSent(NewsletterMail::class, 5); }测试环境配置测试必须运行在隔离环境中避免触碰开发/生产数据。技能文档给出两份关键配置。phpunit.xml?xml version1.0 encodingUTF-8? phpunit xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:noNamespaceSchemaLocationvendor/phpunit/phpunit/phpunit.xsd bootstrapvendor/autoload.php colorstrue testsuites testsuite nameUnit directory suffixTest.phptests/Unit/directory /testsuite testsuite nameFeature directory suffixTest.phptests/Feature/directory /testsuite /testsuites php env nameAPP_ENV valuetesting/ env nameBCRYPT_ROUNDS value4/ env nameCACHE_STORE valuearray/ env nameDB_CONNECTION valuesqlite/ env nameDB_DATABASE value:memory:/ env nameMAIL_MAILER valuearray/ env nameQUEUE_CONNECTION valuesync/ env nameSESSION_DRIVER valuearray/ /php /phpunit要点DB_CONNECTIONsqliteDB_DATABASE:memory:让测试跑在内存数据库中CACHE_STOREarray、SESSION_DRIVERarray、MAIL_MAILERarray与QUEUE_CONNECTIONsync共同确保测试期间无外部副作用。基础 TestCase在基类TestCase中集中提供认证辅助方法减少重复namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { protected function setUp(): void { parent::setUp(); // 仅在测试非 HTTP 异常时调用 $this-withoutExceptionHandling() // 它会抑制 assertStatus() 等断言行为 } protected function actingAsUser(): mixed { $user \App\Models\User::factory()-create(); $this-actingAs($user); return $user; } protected function actingAsAdmin(): mixed { $admin \App\Models\User::factory()-admin()-create(); $this-actingAs($admin); return $admin; } }覆盖率目标与命令技能文档给出明确的覆盖率门槛总体 80%unit feature并按组件细化目标组件目标Models95%Actions/Services90%Form Requests90%Controllers85%Policies95%总体80%生成覆盖率报告的命令# PHPUnitclover 输出可用于 CI 阈值检查 vendor/bin/phpunit --coverage-html coverage --coverage-clover clover.xml # Pest内置阈值 vendor/bin/pest --coverage --min80CI 中应使用pcov或XDEBUG_MODEcoverage收集覆盖率且阈值应固化在 CI 配置中而不是靠口头约定rules/php/testing.md。常用测试命令php artisan test # Laravel 官方测试入口 vendor/bin/phpunit # 直接运行 PHPUnit vendor/bin/pest # 直接运行 Pest测试最佳实践DO / DONT应当用 Factory 而非手工create()拼数据每条测试只做一个逻辑断言使用描述性命名test_guests_cannot_create_products覆盖边界条件与授权边界用Http::fake()、Mail::fake()等 Mock 外部服务用RefreshDatabase保证干净状态。不应不测试 Laravel 框架内部实现信任框架不让测试相互依赖不过度 Mock——只在服务边界 Mock不测试私有方法——通过公开接口测试不让测试耦合 HTML 结构。快速参考表模式用途RefreshDatabase测试间重置数据库$this-actingAs($user)以指定用户认证$this-withToken($token)API Bearer token 认证Model::factory()-create()用工厂创建模型Model::factory()-count(5)-create()批量创建Http::fake([...])Mock HTTP 调用Mail::fake()捕获发出的邮件Notification::fake()捕获通知Queue::fake()捕获入队任务Event::fake()捕获分发的事件Storage::fake(public)捕获文件操作assertDatabaseHas断言数据库行存在assertSoftDeleted断言软删除assertSessionHasErrors断言校验错误assertForbidden断言 403与 ECC 配套技能的衔接laravel-tdd并非孤立存在它处于 ECC 的完整测试闭环中tdd-workflowskills/tdd-workflow/SKILL.md仓库级的 RED → GREEN → REFACTOR 循环要求 80% 覆盖率unit integration E2E并规定 RED 必须先验证、每个阶段留下 checkpoint 提交、最后产出 TDD 证据报告laravel-verificationskills/laravel-verification/SKILL.md合入前/发布前的完整验证流水线——环境检查php -v、composer --version、php artisan --version→composer validate→vendor/bin/pint --test与vendor/bin/phpstan analyse→XDEBUG_MODEcoverage php artisan test --coverage→composer audit→php artisan migrate --pretend→ 缓存预热与队列/调度检查laravel-securityskills/laravel-security/SKILL.md认证、授权与安全编码laravel-patternsLaravel 架构、Eloquent、路由与 API 模式。当 ECC 通过/project-init依据 config/project-stack-mappings.json 识别到 PHP/Laravel 项目时以上技能会整体装配tdd-workflow中的测试运行器探测如 scripts/setup-package-manager.js 的--detect模式会先解析包管理器与测试运行器再把 RED/GREEN 门禁、覆盖率阈值与证据报告串联成可重复执行的开发流程。结语laravel-tdd提供的不是零散断言技巧而是一套完整的测试决策框架何时选 Pest 或 PHPUnit、何时用哪套数据库策略、哪类副作用该用哪个 Fake、覆盖率目标定在哪里。结合 ECC 的tdd-workflow与laravel-verification它把先写测试从个人习惯升级为可验证、可留痕、可合入门禁的工程流程。对任何正在用 Laravel 交付 API、后台或全栈应用的团队这套模式都可以直接落地为日常开发的标准动作。【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价