一套能直接开源落地的 Hyperf API 契约测试平台方案: 目标是做成一个独立服务,支持 导入契约(OpenAPI)、执行回归测试、比对响应、生成报告、持续集成触发。 ---1)项目定位(MVP) 先做5个能力:1. 契约管理(OpenAPI JSON/YAML,版本化)2. 用例管理(从契约生成 + 手工补充)3. 执行引擎(发请求、断言状态码/JSON Schema/关键字段)4. 报告中心(通过/失败、差异详情)5. CI 接入(PR 或主干提交自动跑) ---2)仓库结构(建议) hyperf-contract-testing/ ├─ app/ │ ├─ Controller/ │ │ ├─ ContractController.php │ │ ├─ TestCaseController.php │ │ └─ RunController.php │ ├─ Model/ │ │ ├─ ApiContract.php │ │ ├─ ContractTestCase.php │ │ └─ ContractTestRun.php │ ├─ Service/ │ │ ├─ OpenApiParser.php │ │ ├─ ContractImporter.php │ │ ├─ ContractTestRunner.php │ │ ├─ SchemaAssertService.php │ │ └─ DiffService.php │ ├─ Job/ │ │ └─ RunContractSuiteJob.php │ └─ Command/ │ └─ RunContractCommand.php ├─ config/autoload/ │ ├─ routes.php │ ├─ databases.php │ └─ async_queue.php ├─ migrations/ ├─ tests/ ├─ .github/workflows/ci.yml ├─ docker-compose.yml ├─ README.md ├─ SECURITY.md └─ LICENSE ---3)从0初始化composercreate-project hyperf/hyperf-skeleton hyperf-contract-testingcdhyperf-contract-testingcomposerrequire hyperf/db-connection hyperf/database hyperf/async-queue hyperf/rediscomposerrequire guzzlehttp/guzzle opis/json-schema symfony/yamlcomposerrequire--devphpunit/phpunit phpstan/phpstan friendsofphp/php-cs-fixer ---4)核心数据表(迁移) api_contracts 契约版本 Schema::create('api_contracts',function(Blueprint$table){$table->bigIncrements('id');$table->string('service_name',100);$table->string('version',50);// 如 v1.2.0 / commit sha$table->string('source_type',20)->default('openapi');$table->longText('raw_content');// 原始 openapi 文档$table->timestamps();$table->unique(['service_name','version']);});contract_test_cases 用例 Schema::create('contract_test_cases',function(Blueprint$table){$table->bigIncrements('id');$table->unsignedBigInteger('contract_id');$table->string('case_name',150);$table->string('method',10);$table->string('path',255);$table->json('headers')->nullable();$table->json('query_params')->nullable();$table->json('request_body')->nullable();$table->unsignedInteger('expected_status')->default(200);$table->json('expected_schema')->nullable();$table->json('expected_fragments')->nullable();// 关键字段断言$table->tinyInteger('enabled')->default(1);$table->timestamps();$table->index(['contract_id','enabled']);});contract_test_runs 执行记录 Schema::create('contract_test_runs',function(Blueprint$table){$table->bigIncrements('id');$table->unsignedBigInteger('contract_id');$table->string('triggered_by',50)->default('manual');// manual/ci$table->unsignedInteger('total')->default(0);$table->unsignedInteger('passed')->default(0);$table->unsignedInteger('failed')->default(0);$table->tinyInteger('status')->default(1);//1=running,2=done,3=error$table->longText('report_json')->nullable();$table->timestamps();});---5)核心代码(可直接改)5.1OpenAPI 导入服务 app/Service/ContractImporter.php<?php declare(strict_types=1);namespace App\Service;use App\Model\ApiContract;use App\Model\ContractTestCase;use Hyperf\DbConnection\Db;class ContractImporter{publicfunction__construct(private OpenApiParser$parser){}publicfunctionimport(string$service, string$version, string$raw): int{returnDb::transaction(function()use($service,$version,$raw){$contract=ApiContract::query()->create(['service_name'=>$service,'version'=>$version,'raw_content'=>$raw,'source_type'=>'openapi',]);$cases=$this->parser->generateCases($raw);foreach($casesas$c){ContractTestCase::query()->create(['contract_id'=>$contract->id,'case_name'=>$c['case_name'],'method'=>$c['method'],'path'=>$c['path'],'expected_status'=>$c['expected_status']??200,'expected_schema'=>$c['expected_schema']?? null,]);}return(int)$contract->id;});}}5.2Runner(执行 + 断言 + 差异) app/Service/ContractTestRunner.php<?php declare(strict_types=1);namespace App\Service;use App\Model\ContractTestCase;use GuzzleHttp\Client;class ContractTestRunner{publicfunction__construct(private SchemaAssertService$schemaAssert, private DiffService$diffService){}publicfunctionrunOne(ContractTestCase$case, string$baseUrl): array{$client=new Client(['timeout'=>10,'http_errors'=>false]);$resp=$client->request($case->method, rtrim($baseUrl,'/').$case->path,['headers'=>$case->headers ??[],'query'=>$case->query_params ??[],'json'=>$case->request_body ?? null,]);$actualStatus=$resp->getStatusCode();$bodyText=(string)$resp->getBody();$json=json_decode($bodyText,true);$errors=[];if($actualStatus!==(int)$case->expected_status){$errors[]="status mismatch: expected={$case->expected_status}, actual={$actualStatus}";}if(!empty($case->expected_schema)){$schemaErrors=$this->schemaAssert->validate($json,$case->expected_schema);$errors=array_merge($errors,$schemaErrors);}if(!empty($case->expected_fragments)){$fragErrors=$this->diffService->assertFragments($json,$case->expected_fragments);$errors=array_merge($errors,$fragErrors);}return['case_id'=>$case->id,'case_name'=>$case->case_name,'pass'=>count($errors)===0,'errors'=>$errors,'actual_status'=>$actualStatus,'actual_body'=>$json,];}}5.3JSON Schema 断言 app/Service/SchemaAssertService.php<?php declare(strict_types=1);namespace App\Service;use Opis\JsonSchema\Validator;class SchemaAssertService{publicfunctionvalidate(mixed$data, array$schema): array{$validator=new Validator();$result=$validator->validate(json_decode(json_encode($data)), json_decode(json_encode($schema)));if($result->isValid()){return[];}return['schema validation failed'];}}5.4任务异步执行 app/Job/RunContractSuiteJob.php<?php declare(strict_types=1);namespace App\Job;use App\Model\ContractTestRun;use App\Model\ContractTestCase;use App\Service\ContractTestRunner;use Hyperf\AsyncQueue\Job;class RunContractSuiteJob extends Job{publicfunction__construct(public int$runId, public int$contractId, public string$baseUrl){}publicfunctionhandle(): void{$run=ContractTestRun::query()->findOrFail($this->runId);$runner=di(ContractTestRunner::class);$cases=ContractTestCase::query()->where('contract_id',$this->contractId)->where('enabled',1)->get();$report=[];$passed=0;foreach($casesas$case){$r=$runner->runOne($case,$this->baseUrl);$report[]=$r;if($r['pass'])$passed++;}$run->total=count($report);$run->passed=$passed;$run->failed=$run->total -$passed;$run->status=2;$run->report_json=json_encode($report, JSON_UNESCAPED_UNICODE);$run->save();}}---6)API 路由(最小闭环) config/autoload/routes.php Router::addGroup('/api/contracts',function(){Router::post('/import',[App\Controller\ContractController::class,'import']);Router::get('/{id:\d+}/cases',[App\Controller\TestCaseController::class,'list']);Router::post('/{id:\d+}/run',[App\Controller\RunController::class,'run']);Router::get('/runs/{runId:\d+}',[App\Controller\RunController::class,'detail']);});---7)CI 集成(开源必须) .github/workflows/ci.yml 最少包含:1.composervalidate2. php-cs-fixer --dry-run3. phpstan4. phpunit5. 启动 demo API + 本平台,跑一次契约执行,校验失败时 CI fail ---8)开源发布流程(完整)1. LICENSE:MIT 或 Apache-2.02. README:5 分钟启动、架构图、示例 OpenAPI、报告截图3. SECURITY.md:漏洞提交流程4. GitHub Issue 模板:bug / feature / contract-support5. 首版 Tag:v0.1.0(声明 API 仍可能调整)6. 每次 Release 写清楚:新增断言能力、破坏性变更、迁移步骤 ---9)持续维护路线图 - v0.1: OpenAPI 导入 + 基础执行 + 报告 - v0.2: 环境矩阵(dev/staging/prod-like)+ webhook 通知 - v0.3: 历史趋势(通过率、接口稳定性排行) - v1.0: 多项目隔离、权限模型、插件化断言器(gRPC/GraphQL) ---10)最容易踩坑的点1. 用例直接绑定真实动态数据,导致频繁误报2. 契约变更没版本化,历史报告不可追溯3. 只比状态码,不比 schema 和关键字段4. 报告无原始响应,排障困难5. CI 超时控制缺失,整条流水线卡住 --- 这套结构可以直接做开源首版。先把 导入器 + Runner + 报告 + CI 跑通,社区就能用;后续再补通知、趋势和权限。