hyperf API 契约测试平台开源完整流程(从 0 到持续维护)==写一个开源项目全流程
一套能直接开源落地的 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);//1running,2done,3error$table-longText(report_json)-nullable();$table-timestamps();});---5)核心代码可直接改5.1OpenAPI 导入服务 app/Service/ContractImporter.php?php declare(strict_types1);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){$contractApiContract::query()-create([service_name$service,version$version,raw_content$raw,source_typeopenapi,]);$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_types1);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{$clientnew Client([timeout10,http_errorsfalse]);$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();$jsonjson_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);$errorsarray_merge($errors,$schemaErrors);}if(!empty($case-expected_fragments)){$fragErrors$this-diffService-assertFragments($json,$case-expected_fragments);$errorsarray_merge($errors,$fragErrors);}return[case_id$case-id,case_name$case-case_name,passcount($errors)0,errors$errors,actual_status$actualStatus,actual_body$json,];}}5.3JSON Schema 断言 app/Service/SchemaAssertService.php?php declare(strict_types1);namespace App\Service;use Opis\JsonSchema\Validator;class SchemaAssertService{publicfunctionvalidate(mixed$data, array$schema): array{$validatornew 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_types1);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{$runContractTestRun::query()-findOrFail($this-runId);$runnerdi(ContractTestRunner::class);$casesContractTestCase::query()-where(contract_id,$this-contractId)-where(enabled,1)-get();$report[];$passed0;foreach($casesas$case){$r$runner-runOne($case,$this-baseUrl);$report[]$r;if($r[pass])$passed;}$run-totalcount($report);$run-passed$passed;$run-failed$run-total -$passed;$run-status2;$run-report_jsonjson_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. LICENSEMIT 或 Apache-2.02. README5 分钟启动、架构图、示例 OpenAPI、报告截图3. SECURITY.md漏洞提交流程4. GitHub Issue 模板bug / feature / contract-support5. 首版 Tagv0.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 跑通社区就能用后续再补通知、趋势和权限。