Testing:PHPUnit 测试改造技术方案
August 1, 2026 · View on GitHub
1. 定位与目标
1.1 改造前问题(已解决)
| 问题 | 改造前 | 现状 |
|---|---|---|
| 无标准测试框架 | 无 PHPUnit / TestCase | PHPUnit 11 + phpunit.xml.dist |
| 脚本式回归 | src/**/Tests/*Test.php 手写 assert | 已迁入 PHPUintTest/,单轨 PHPUnit |
| CI 难接入 | @php file.php 串联 | composer test / suite / group;可选 test:coverage |
| 分层缺失 | Unit/协程/HTTP/Redis 混跑 | suite 分层 + @group 横切 exclude |
| HTTP 靠人工 curl | 先启服再手测 | PHPUintTest/Http + Controller curl 黄金路径 |
| 协程难回归 | 无统一基类 | CoroutineTestCase + 可选泄漏断言 |
1.2 改造目标
- 全面接入 PHPUnit 11,作为唯一推荐运行器。
- 三层套件:Unit / CoroutineUnit / HttpIntegration(+ Websocket);分层用 suite,横切用 group。
- HTTP 全流程 = 自动化 curl:真启
Test服务 + Guzzle 打/api/v1/*,覆盖路由→中间件→Controller→JSON。 - 迁移完成:旧
src/**/Tests/*Test.phpdeprecate 转发已删除;composer test:*/phpunit --filter为唯一入口。 - 默认 CI 绿灯:
composer test只跑unit+coroutine;Http/Websocket 不进默认 suite(不用 group exclude 挡);Redis/DB 用@groupexclude。
1.3 非目标(MVP 不做)
| 不做 | 原因 |
|---|---|
进程内伪造完整 onRequest 路由/中间件链 | 耦合底层;P6 仅提供 HttpRequestHarness(RequestInput),全链路仍靠 HttpIntegration |
一次迁完所有 Test/Scripts CLI | 依赖 Script 进程,优先级低于 Support + Http |
| 强制覆盖率百分比门槛 | 提供 composer test:coverage 报告,不设 min % 门禁 |
| 改业务行为 | 测试基建不夹带功能变更 |
2. 现有资产盘点(改造输入)
2.1 composer 脚本(现状:PHPUnit filter / suite)
见 composer.json scripts:
| Script | 覆盖 |
|---|---|
test | --testsuite unit,coroutine(默认绿灯) |
test:http / test:http:ci | Http suite;CI 模式 B 用后者(AUTO_START + 不 skip) |
test:websocket | Offline + Cluster 等(默认排除 redis/smoke) |
test:coverage | unit+coroutine 文本覆盖率(需 pcov/xdebug;无百分比门禁) |
test:workflow | Phase1–4、Integration、RunStore、HitlAuth、PluginMemory |
test:mqtt | MqttModuleTest、MqttGracefulShutdownTest |
test:job / test:agent / test:ai / test:mcp / … | 各 Support 模块 |
test:phase-a … test:phase-d | 生产加固 |
test:support | 聚合 Support 相关 filter |
test:module-workflows / test:*-workflow | PHPUintTest/Unit/Module/*(含 Contract) |
未进默认 composer test: Http / Websocket suite;@group redis|db|slow|smoke(含 RedisRunStoreCasTest、MQTT/WS 可选 smoke)。
2.2 脚本测试标准形态(迁移前)
// 以 JobPhase1Test / WorkflowHitlAuthTest 为代表
require vendor/autoload.php;
function assertTrue(bool $c, string $m): void { if (!$c) throw new RuntimeException($m); }
function pass(string $name): void { echo "[PASS] {$name}\n"; }
function testEnvelopeRoundTrip(): void { /* … */ assertTrue(...); }
$tests = ['envelope' => 'testEnvelopeRoundTrip', /* … */];
foreach ($tests as $name => $fn) {
$fn();
pass($name);
}
迁移映射规则:
| 脚本元素 | PHPUnit |
|---|---|
function testXxx() | public function testXxx(): void |
assertTrue($c, $msg) | $this->assertTrue($c, $msg) 或更具体的 assertSame |
pass($name) | 删除(PHPUnit 报告替代) |
文件尾 foreach | 删除(发现机制替代) |
文件级 require autoload | PHPUintTest/bootstrap.php(composer 仅 Swoolefy;Test\ / PHPUintTest\ 各自 autoloader) |
| 顶部业务注释「覆盖范围」 | 类 PHPDoc 保留 |
2.3 已有可复用基建
| 资产 | 路径 | 改造中的角色 |
|---|---|---|
| Support 协程 stub | src/Support/Tests/SwoolefyTestBootstrap.php | CoroutineTestCase::setUp 必引 |
| WS 探活 | PHPUintTest/Websocket/Support/SmokeTestSupport.php | Http/Ws *ServerManager 范本 |
| HTTP Demo + curl | Test/Module/、docs/AI-WORKFLOW.md | HttpIntegration 用例来源 |
| Auth 验收 | docs/Auth.md | CoroutineUnit(goApp 透传)+ Http(Bearer) |
| Guzzle | composer.json require | Http 客户端,无需新增依赖(PHPUnit 除外) |
2.4 为何 HTTP 必须打真端口
swoolefy 请求路径:HttpServer::onRequest → Bootstrap / HeaderContext → 路由中间件 → Controller。
没有 Laravel 式进程内 Kernel::handle。因此:
| 测法 | 覆盖路由/中间件/JSON | 改造定位 |
|---|---|---|
| 真服务 + Guzzle(等价 curl) | 完整 | HttpIntegration MVP |
new Controller + 假 RequestInput | 否 | 仅补充 Unit |
| 进程内伪造 Swoole Request | 理论完整 | Phase 后期可选 harness |
3. 目标架构
flowchart TB
subgraph runner [PHPUnit]
U[testsuite unit]
C[testsuite coroutine]
H[testsuite http]
W[testsuite websocket]
end
New["PHPUintTest/ 单轨入口"]
runner --> New
H --> Srv["cli.php start Test :9501 或 AUTO_START"]
W --> WsSrv["WebsocketService"]
3.1 分层定义
| 层 | 目录 | 基类 | 依赖 | 默认 CI |
|---|---|---|---|---|
| Unit | PHPUintTest/Unit/ | PHPUintTest\TestCase | 无协程调度、无网络 | 是 |
| CoroutineUnit | PHPUintTest/Coroutine/ | CoroutineTestCase | ext-swoole、SwoolefyTestBootstrap | 是 |
| HttpIntegration | PHPUintTest/Http/ | HttpIntegrationTestCase | 真 HTTP 服务 | 否(独立 suite http) |
| Websocket | PHPUintTest/Websocket/ | 复用探活 | WebsocketService | 否(独立 suite websocket) |
| Redis/DB | 任意层,标 @group redis/db | — | 中间件 | 默认 group exclude(横切) |
隔离约定(已定):
| 维度 | 手段 | 例子 |
|---|---|---|
| 分层(目录) | suite | unit / coroutine / http / websocket |
| 横切(依赖) | @group + exclude | redis / db / slow |
- Http 用例不要再标会被默认 exclude 的
@group http(否则与 suite 叠用会踩坑)。 - 可用
@group outdoor/@group workflow等业务标签做--groupfilter。
3.2 目录落位(新建)
phpunit.xml.dist
PHPUintTest/
bootstrap.php
TestCase.php
CoroutineTestCase.php
Http/
HttpIntegrationTestCase.php
Support/
HttpServerManager.php
HttpServerUnavailableException.php
OutdoorWorkflowHttpTest.php
WorkflowHttpTest.php # list/run/resume + HITL 错 Key(gated)
OrderWorkflowHttpTest.php # process mock + saga
Unit/Controller/ # Common Controller curl(归 http suite)
Support/
HttpRequestHarness.php # P6:进程内 RequestInput(非完整 onRequest)
Unit/
Mqtt/ …
Support/ … # Job/Workflow/Agent/… + HttpRequestHarnessTest
Module/ # Outdoor|Order|Research|Rag|Contract|Knowledge|Workflow
Coroutine/
Support/Auth/AuthContextGoAppTest.php
… # 协程生命周期等
Websocket/ # suite websocket;Smoke @group smoke
# 单轨:PHPUintTest/ 为唯一测试入口;src/**/Tests 仅 Fixtures/Bootstrap
composer 仅映射框架本体;Demo / PHPUnit 命名空间各自注册:
{
"autoload": {
"psr-4": {
"Swoolefy\\": "src/"
}
}
}
| 命名空间 | 注册入口 |
|---|---|
Swoolefy\ | vendor/autoload.php(composer autoload.psr-4) |
Test\ | Test\Autoloader(Test/Autoloader.php;cli.php registerNamespace;PHPUnit/PhpStorm 经 autoload-dev.files) |
PHPUintTest\ | PHPUintTest\Autoloader(经 PHPUintTest/register_dev_autoload.php → autoload-dev.files) |
业务应用 App\ 等 | \<AppName>\Autoloader(create 自根目录 Autoloader.php 模板复制) |
PhpStorm 单独 Run method 时若只挂 vendor/autoload.php,依赖上述 autoload-dev.files;改完后执行一次 composer dump-autoload。也可在 PHPUnit 配置里指定 Default configuration file = phpunit.xml.dist。
4. 核心接口与基类契约
4.1 TestCase
namespace PHPUintTest;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
// 禁止再定义文件级 assertTrue();统一用 PHPUnit 断言
}
4.2 CoroutineTestCase
namespace PHPUintTest;
use Swoole\Coroutine;
use Swoole\Runtime;
abstract class CoroutineTestCase extends TestCase
{
protected function setUp(): void
{
parent::setUp();
require_once dirname(__DIR__, 2) . '/src/Support/Tests/SwoolefyTestBootstrap.php';
Runtime::enableCoroutine(true);
}
protected function runInCoroutine(callable $fn): mixed
{
$result = null;
$error = null;
Coroutine\run(static function () use ($fn, &$result, &$error): void {
try {
$result = $fn();
} catch (\Throwable $e) {
$error = $e;
}
});
if ($error instanceof \Throwable) {
throw $error;
}
return $result;
}
}
用途: goApp / GoWaitGroup / Context / Auth array 透传 / 协程内 Application::getApp()->get('db') 隔离。
4.3 HttpIntegrationTestCase(curl → PHPUnit)
namespace PHPUintTest\Http;
use GuzzleHttp\Client;
use PHPUintTest\TestCase;
/**
* Http 全流程基类。
* 靠 suite「http」隔离,勿标会被默认 exclude 的 @group http。
* 业务可另标 @group outdoor / @group workflow 等。
*/
abstract class HttpIntegrationTestCase extends TestCase
{
protected static Client $http;
protected static string $baseUrl;
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
self::$baseUrl = rtrim(
(string) (getenv('SWOOLEFY_TEST_BASE_URL') ?: 'http://127.0.0.1:9501'),
'/'
);
try {
HttpServerManager::ensureAvailable(self::$baseUrl);
} catch (HttpServerUnavailableException $e) {
self::markTestSkipped($e->getMessage());
}
self::$http = new Client([
'base_uri' => self::$baseUrl . '/',
'http_errors' => false,
'timeout' => 30,
]);
}
protected function postJson(string $path, array $body = [], array $headers = []): array
{
$res = self::$http->post(ltrim($path, '/'), [
'headers' => array_merge(['Content-Type' => 'application/json'], $headers),
'json' => $body,
]);
$raw = (string) $res->getBody();
$json = json_decode($raw, true);
return [
'status' => $res->getStatusCode(),
'body' => is_array($json) ? $json : $raw,
'headers' => $res->getHeaders(),
];
}
protected function getJson(string $path, array $headers = []): array { /* 同理 */ }
}
4.4 HttpServerManager:两种启服模式
| 模式 | 适用 | 行为 |
|---|---|---|
| A 外部启服(默认) | 本地开发 | 开发者先 php cli.php start Test;探活失败且 SWOOLEFY_HTTP_SKIP_IF_DOWN=1 → skip |
| B 自动拉起 | CI | SWOOLEFY_HTTP_AUTO_START=1 → 后台 start,suite 结束 stop;探活失败则 fail(CI 设 SKIP_IF_DOWN=0) |
环境变量:
| 变量 | 默认 | 含义 |
|---|---|---|
SWOOLEFY_TEST_BASE_URL | http://127.0.0.1:9501 | HTTP 基址 |
SWOOLEFY_HTTP_AUTO_START | 0 | 是否自动 start/stop |
SWOOLEFY_HTTP_SKIP_IF_DOWN | 1(本地 phpunit.xml) | 不可达则 skip;CI Http job 建议 0 |
SWOOLEFY_HTTP_READY_TIMEOUT | 30 | 探活最长等待秒数 |
SWOOLEFY_HTTP_HITL_AUTH | 0 | PHPUnit 侧声明服务端已开 HITL(配合 WORKFLOW_HITL_AUTH_ENABLED=1) |
SWOOLEFY_HTTP_SHARED_RUN_STORE | 0 | 声明服务端为共享 RunStore,允许跨请求 status/resume |
WS_HOST / WS_PORT / WS_SMOKE_SKIP_IF_DOWN | 沿用现网 | Websocket suite |
模式 B 实现要点(贴合 swoolefy CLI):
- 实现:
HttpServerManager在SWOOLEFY_HTTP_AUTO_START=1时执行php cli.php restart Test --force=1 --daemon=1。 - 探活:轮询
GET {baseUrl}/,直到可读或超时(SWOOLEFY_HTTP_READY_TIMEOUT)。 - 日志:stdout/stderr →
Test/Storage/Logs/phpunit-http.log。 - 收尾:仅当我们拉起的进程才
stop Test --force=1(register_shutdown_function)。 - 入口:
composer test:http:ci(已设 AUTO_START=1、SKIP_IF_DOWN=0)。 - 端口:固定 Test
9501;CI runner 独占。 - 目标环境:macOS / Linux;不适配 Windows。
5. HTTP 全流程改造详解(重点)
5.1 从 curl 到用例的固定模板
改造前(文档):
php cli.php start Test
curl -s -X POST "http://127.0.0.1:9501/api/v1/outdoor/workflow/cycling" \
-H "Content-Type: application/json" \
-d '{"destination":"深圳湾公园","weatherHint":"sunny","useMock":true}'
改造后(PHPUnit):
namespace PHPUintTest\Http;
/** @group outdoor */
final class OutdoorWorkflowHttpTest extends HttpIntegrationTestCase
{
public function testCyclingSunnyReturnsRunId(): void
{
$res = $this->postJson('/api/v1/outdoor/workflow/cycling', [
'destination' => '深圳湾公园',
'weatherHint' => 'sunny',
'useMock' => true,
]);
$this->assertSame(200, $res['status']);
$this->assertIsArray($res['body']);
$runId = $res['body']['runId']
?? $res['body']['data']['runId']
?? null;
$this->assertNotEmpty($runId);
}
}
HITL:
$res = $this->postJson('/api/v1/workflow/resume', [
'runId' => $runId,
], [
'X-Workflow-Api-Key' => getenv('WORKFLOW_HITL_API_KEY') ?: 'test-hitl-key',
]);
Auth 已落地(见 docs/Auth.md),Bearer / 缺 token 401 可直接写 Http 样板:
// 缺 Bearer → HTTP 401(GET /api/auth-user/me)
$res = $this->getJson('/api/auth-user/me');
$this->assertSame(401, $res['status']);
// 合法 JWT
$res = $this->postJson('/api/v1/...', $body, [
'Authorization' => 'Bearer ' . $jwt,
]);
5.2 断言边界(防脆测)
| 应断言 | 不应断言 |
|---|---|
| HTTP status、业务 code | 完整 LLM / OCR 长文本 |
runId / status 字段存在与枚举 | 绝对耗时(除非超时用例) |
| 401/403 鉴权失败 | 内部 spl_object_id |
| Header 透传关键键(若契约要求) | 日志文件内容 |
5.3 首批 Http 用例清单(Phase 3)
| 优先级 | 来源 | 用例 |
|---|---|---|
| P0 | Outdoor | ✅ OutdoorWorkflowHttpTest:sunny / rainy / status 缺 runId |
| P0 | Workflow | ✅ WorkflowHttpTest:list;contract_review run;resume(共享 Store);status 缺 runId |
| P0 | Auth / Common Controller | ✅ PHPUintTest/Unit/Controller/*(suite=http);Redis/Cache 标 redis |
| P1 | HITL curl | ✅ 错 Key → 403(SWOOLEFY_HTTP_HITL_AUTH=1 + 服务端 WORKFLOW_HITL_AUTH_ENABLED=1;status 主路径;resume 另需共享 Store) |
| P2 | Order saga demo | ✅ OrderWorkflowHttpTest:process mock 批准;saga → 400;status 缺 runId |
引擎边角、HITL 纯逻辑仍留在 Unit(WorkflowHitlAuthTest),Http 只保契约。
5.4 与直接测 Controller 的分工
Unit: WorkflowEngine / HitlAuth / JobRunner ← 快、密
Http: /api/v1/outdoor/workflow/cycling ← 少而稳(黄金路径)
禁止「所有分支都打 HTTP」。
6. 脚本 → PHPUnit 改造规程(Support)
6.1 单文件改造步骤(Checklist)
-
新建
PHPUintTest/Unit/Support/{Module}/{Name}Test.php,extends TestCase。 -
将每个
function testXxx()变为类方法;assertTrue→$this->assert*。 -
删除文件级
assertTrue/pass/ 尾部foreach。 -
Stub / Fake 类移入同文件 private class 或
PHPUintTest/Unit/Support/{Module}/Fixture/。 -
删除旧
src/**/Tests/*Test.php转发脚本;composer test:{module}→phpunit --testsuite unit --filter X。 -
跑通:
./vendor/bin/phpunit --filter JobPhase1Test与旧脚本结果一致后合并。
6.2 模块迁移优先级
| 批次 | 模块 | 理由 |
|---|---|---|
| 1(样板) | Job Phase1、WorkflowHitlAuth | 无 IO、断言清晰 |
| 2 | Workflow Phase1–4、Integration、PluginMemory | composer 主回归 |
| 3 | Agent/AI/Mcp/Neuron/Rag/Capability/DocumentOcr | 同模式批量 |
| 4 | Phase A–D、SupportLog | 注意部分需 Coroutine |
| 5 | Websocket 离线单测 → Unit;Smoke → Websocket suite | |
| 6 | RedisRunStoreCas、Cluster → #[Group('redis')] | ✅ |
| 7 | Test/Module/* Demo 工作流 → PHPUintTest/Unit/Module | ✅ |
6.3 协程单测迁入规范
凡内部已有 Coroutine\run 的,迁入 PHPUintTest/Coroutine/,外层用 runInCoroutine 单层调度:
- 脚本原文若已包一层
Coroutine\run,迁入时去掉内层,只保留runInCoroutine(fn…)。 - 禁止
runInCoroutine里再调Coroutine\run(双重嵌套在部分 Swoole 版本上行为不稳定)。 Runtime::enableCoroutine(true)放在CoroutineTestCase::setUp;勿在每个用例里反复开关。
7. phpunit.xml.dist 与 composer 命令
7.1 phpunit.xml.dist(草案)
隔离策略:分层靠 suite,横切靠 group。
默认命令只指定 unit,coroutine;Http/Websocket 不在默认 suite 扫描范围内,不要用 <group>http</group> exclude(否则 --testsuite http 也会被全局 exclude 掉)。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="PHPUintTest/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache"
failOnWarning="false">
<!-- 过渡期 failOnWarning=false,避免 Swoole/扩展 notice 误伤;稳定后可改 true -->
<testsuites>
<testsuite name="unit">
<directory>PHPUintTest/Unit</directory>
</testsuite>
<testsuite name="coroutine">
<directory>PHPUintTest/Coroutine</directory>
</testsuite>
<testsuite name="http">
<directory>PHPUintTest/Http</directory>
</testsuite>
<testsuite name="websocket">
<directory>PHPUintTest/Websocket</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<!-- 仅横切依赖;勿 exclude http/websocket(已由 suite 隔离) -->
<group>redis</group>
<group>db</group>
<group>slow</group>
</exclude>
</groups>
<php>
<env name="SWOOLEFY_CLI_ENV" value="dev"/>
<env name="SWOOLEFY_HTTP_SKIP_IF_DOWN" value="1"/>
</php>
<source>
<include>
<directory suffix=".php">src/Support</directory>
<directory suffix=".php">src/Http</directory>
<directory suffix=".php">src/Core</directory>
<directory suffix=".php">src/Websocket</directory>
</include>
</source>
</phpunit>
| 命令 | 跑什么 |
|---|---|
composer test / phpunit --testsuite unit,coroutine | 默认绿灯(扫不到 Http/Websocket 目录) |
phpunit --testsuite http | 全流程;不依赖 group exclude |
phpunit --group redis | 显式跑 Redis 横切用例(需去掉 exclude 或 --group redis 覆盖,按 PHPUnit 版本选用) |
7.2 composer scripts(已落地)
{
"test": "phpunit --testsuite unit,coroutine",
"test:unit": "phpunit --testsuite unit",
"test:coroutine": "phpunit --testsuite coroutine",
"test:http": "phpunit --testsuite http",
"test:http:ci": "SWOOLEFY_HTTP_AUTO_START=1 SWOOLEFY_HTTP_SKIP_IF_DOWN=0 phpunit --testsuite http",
"test:websocket": "phpunit --testsuite websocket",
"test:coverage": "phpunit --testsuite unit,coroutine --coverage-text"
}
模块级 filter(test:workflow / test:mqtt / test:module-workflows 等)保留,便于本地窄跑。
7.3 日常命令
# 默认(快)——只跑 unit + coroutine
composer test
# HTTP 全流程(模式 A:先启服)
php cli.php start Test
composer test:http
# HTTP 全流程(模式 B:CI 自动启停)
composer test:http:ci
# HITL 错 Key 403(需服务端开启鉴权)
WORKFLOW_HITL_AUTH_ENABLED=1 php cli.php start Test
SWOOLEFY_HTTP_HITL_AUTH=1 composer test:http -- --filter testHitlWrongApiKey
# 覆盖率文本报告(需启用 pcov 或 xdebug;无 min % 门禁)
composer test:coverage
8. 协程与泄漏检测(改造加分项)
迁入 CoroutineUnit 时固化以下用例类型:
| 检测 | 断言思路 |
|---|---|
| Db 协程单例隔离 | 父/子 spl_object_id 不同;同协程两次 get 相同 |
| Context / Auth 不串 | 两并发写入不同 userId,互读隔离 |
| goApp array 透传 | FrameworkContext::setUser 后子协程 getUserId() 非空(见 Auth) |
| 协程泄漏(可选) | CoroutineTestCase::assertCoroutineLeakWithin($fn, $maxDelta) |
禁止在测试里 use ($db) 把父协程连接带进 goApp(与生产禁忌一致)。
9. 分阶段实施路线
| Phase | 内容 | 状态 |
|---|---|---|
| P0 脚手架 | phpunit/phpunit;phpunit.xml.dist(suite 隔离);PHPUintTest/bootstrap + 三基类 | ✅ |
| P1 样板迁移 | JobPhase1 + WorkflowHitlAuth → PHPUintTest/Unit;旧脚本 deprecate 转发 | ✅ |
| P2 Coroutine | CoroutineTestCase + Auth goApp / Context array | ✅ |
| P3 Http | Auth /api/auth-user/me 401 + Outdoor cycling + Workflow list/run/status/resume | ✅ |
| P4 批量 Support | test:support 模块迁入 PHPUintTest/;composer test:* 切 phpunit | ✅ |
| P4+ Module + Redis CAS | Test/Module/* → PHPUintTest/Unit/Module;RedisRunStoreCas + #[Group('redis')] | ✅ |
| P5 Websocket | Offline + Smoke → PHPUintTest/Websocket | ✅ |
| P5 单轨 | 删除 deprecate wrapper;Mqtt → PHPUintTest/Unit/Mqtt;composer test:mqtt | ✅ |
| P6 | HttpRequestHarness(RequestInput);composer test:http:ci;test:coverage(无强制 min %) | ✅ |
| P6 非目标 | 完整伪造 onRequest 路由链;覆盖率百分比门禁 | 不做 |
composer test # unit + coroutine(含 Module / Mqtt / Harness;排除 redis/db/slow/smoke)
composer test:http # 模式 A:先 php cli.php start Test
composer test:http:ci # 模式 B:AUTO_START + SKIP_IF_DOWN=0
composer test:websocket # Offline 等;--group redis / smoke 另开
composer test:mqtt
composer test:module-workflows
composer test:coverage
composer test:job
10. 验收标准
- 无 HTTP 服务时,
composer test(--testsuite unit,coroutine)全部通过,且不会执行PHPUintTest/Http。 - 服务未启 +
SWOOLEFY_HTTP_SKIP_IF_DOWN=1→composer test:httpskip,进程 exit 0。 start Test后 Outdoor sunny cycling:status=200且存在runId。GET /api/auth-user/me无 Bearer → HTTP 401;HITL 错误 API Key → 403(需WORKFLOW_HITL_AUTH_ENABLED=1+SWOOLEFY_HTTP_HITL_AUTH=1)。- 样板模块(JobPhase1、HitlAuth、Contract Module)PHPUnit 可独立 filter 跑通。
- Coroutine:Auth
setUser后 goApp 子协程可读getUserId();可选assertCoroutineLeakWithin。 - 本地模式 A(
test:http)、CI 模式 B(test:http:ci/ AUTO_START + stop)均有文档与可操作步骤。 HttpRequestHarness可构造RequestInput(见HttpRequestHarnessTest);不替代 HttpIntegration。
11. 风险与对策
| 风险 | 对策 |
|---|---|
| 端口占用 / 僵尸 Worker | AUTO_START 必须配对 stop/--force;固定 9501;CI 独占 runner |
| group exclude 误伤 Http | 已定:Http 只用 suite,不 exclude @group http |
双重 Coroutine\run | 迁入时剥掉脚本内层,只留 runInCoroutine |
| 测试污染 DB/Redis | Http 用 mock/useMock;写库用例 @group db + 测试库 |
| 迁移动作误改业务 | PR 只含测试文件与 composer;禁止顺手改 Controller |
| 旧脚本路径被收藏 | 已单轨;文档与 README 统一 composer test:* |
| flaky Http(慢/偶发) | 少而稳的黄金路径;超时标 @group slow(默认 exclude) |
failOnWarning 误伤 | 过渡期 false,收尾再收紧 |
12. 禁忌
| 禁止 | 原因 |
|---|---|
| 默认 CI 启 HTTP/Redis | 破坏快速绿灯 |
用 group exclude 挡 @group http | 与 --testsuite http 冲突,用例会被全 skip |
| 全部逻辑只靠 HttpIntegration | 慢、脆、难定位 |
| Unit 隐式依赖 9501 | 环境耦合 |
| Context 存 AuthUser 对象 做透传测 | goApp 跳过 object(见 Auth) |
| static 挂「当前用户」于测试基类 | 用例间串态 |
| Http 断言大模型全文 | 非稳定契约 |
runInCoroutine 内再套 Coroutine\run | 双重调度不稳定 |
13. 相关文件与交叉引用
| 文件 | 说明 |
|---|---|
| docs/PHPUnitTest.md | 本文:PHPUnit 改造方案 |
| composer.json | 现有 test:* |
| src/Support/Tests/SwoolefyTestBootstrap.php | 协程 stub |
| PHPUintTest/Websocket/Support/SmokeTestSupport.php | 活服务探活 |
| docs/AI-WORKFLOW.md | curl → Http 用例来源 |
| docs/Auth.md | Auth / goApp / Bearer / /api/auth-user/me |
| docs/CapabilityTool.md | Capability 单测迁入 Unit |
| docs/Job.md | Job 模块与 Phase 测试说明 |
| Test/Module/ | Demo 与 README curl |
| Test/Controller/AuthUserController.php | Auth Http 联调样板 |
| README.md | 启服与文档索引(实现脚手架后补链接) |
与 Auth: CoroutineUnit 覆盖 setUser array 透传;HttpIntegration 覆盖 Bearer 与缺 token 401。
与 Workflow: 引擎/HITL 逻辑 → Unit;/api/v1/workflow/* → Http。
入口: PHPUintTest/ + composer test:* 单轨;src/**/Tests 仅保留 Fixtures / Bootstrap / SchemaInstaller。
14. 评审决议与第一步
已决议
| 议题 | 决议 |
|---|---|
| Http / Websocket 如何不进默认 CI | suite 隔离:composer test = unit,coroutine;不用 group exclude 挡 http |
@group 用途 | 仅横切:redis / db / slow;业务 filter 可用 outdoor / workflow |
| Auth | 已落地,P2/P3 直接写 goApp + /api/auth-user/me 用例 |
| 启服 | 本地默认模式 A;CI 用 composer test:http:ci(restart --force + daemon + 配对 stop);不适配 Windows |
落地状态(P0–P6)
- ✅ PHPUnit 11 + suite 隔离 + 三基类 / Http 基类
- ✅ Support / Module(含 Contract)/ Mqtt / Websocket 单轨
PHPUintTest/ - ✅ Http 黄金路径(Outdoor / Workflow / Order / Controller)+ HITL 错 Key(gated)
- ✅ P6:
HttpRequestHarness、test:http:ci、test:coverage(无强制门槛)