Testing:PHPUnit 测试改造技术方案

August 1, 2026 · View on GitHub

1. 定位与目标

1.1 改造前问题(已解决)

问题改造前现状
无标准测试框架无 PHPUnit / TestCasePHPUnit 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 改造目标

  1. 全面接入 PHPUnit 11,作为唯一推荐运行器。
  2. 三层套件:Unit / CoroutineUnit / HttpIntegration(+ Websocket);分层用 suite,横切用 group
  3. HTTP 全流程 = 自动化 curl:真启 Test 服务 + Guzzle 打 /api/v1/*,覆盖路由→中间件→Controller→JSON。
  4. 迁移完成:旧 src/**/Tests/*Test.php deprecate 转发已删除;composer test:* / phpunit --filter 为唯一入口。
  5. 默认 CI 绿灯composer test 只跑 unit+coroutine;Http/Websocket 不进默认 suite(不用 group exclude 挡);Redis/DB 用 @group exclude。

1.3 非目标(MVP 不做)

不做原因
进程内伪造完整 onRequest 路由/中间件链耦合底层;P6 仅提供 HttpRequestHarnessRequestInput),全链路仍靠 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:ciHttp suite;CI 模式 B 用后者(AUTO_START + 不 skip)
test:websocketOffline + Cluster 等(默认排除 redis/smoke
test:coverageunit+coroutine 文本覆盖率(需 pcov/xdebug;无百分比门禁)
test:workflowPhase1–4、Integration、RunStore、HitlAuth、PluginMemory
test:mqttMqttModuleTest、MqttGracefulShutdownTest
test:job / test:agent / test:ai / test:mcp / …各 Support 模块
test:phase-atest:phase-d生产加固
test:support聚合 Support 相关 filter
test:module-workflows / test:*-workflowPHPUintTest/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 autoloadPHPUintTest/bootstrap.php(composer 仅 Swoolefy;Test\ / PHPUintTest\ 各自 autoloader)
顶部业务注释「覆盖范围」类 PHPDoc 保留

2.3 已有可复用基建

资产路径改造中的角色
Support 协程 stubsrc/Support/Tests/SwoolefyTestBootstrap.phpCoroutineTestCase::setUp 必引
WS 探活PHPUintTest/Websocket/Support/SmokeTestSupport.phpHttp/Ws *ServerManager 范本
HTTP Demo + curlTest/Module/docs/AI-WORKFLOW.mdHttpIntegration 用例来源
Auth 验收docs/Auth.mdCoroutineUnit(goApp 透传)+ Http(Bearer)
Guzzlecomposer.json requireHttp 客户端,无需新增依赖(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
UnitPHPUintTest/Unit/PHPUintTest\TestCase无协程调度、无网络
CoroutineUnitPHPUintTest/Coroutine/CoroutineTestCaseext-swooleSwoolefyTestBootstrap
HttpIntegrationPHPUintTest/Http/HttpIntegrationTestCase真 HTTP 服务否(独立 suite http
WebsocketPHPUintTest/Websocket/复用探活WebsocketService否(独立 suite websocket
Redis/DB任意层,标 @group redis/db中间件默认 group exclude(横切)

隔离约定(已定):

维度手段例子
分层(目录)suiteunit / coroutine / http / websocket
横切(依赖)@group + excluderedis / db / slow
  • Http 用例不要再标会被默认 exclude 的 @group http(否则与 suite 叠用会踩坑)。
  • 可用 @group outdoor / @group workflow 等业务标签做 --group filter。

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\AutoloaderTest/Autoloader.php;cli.php registerNamespace;PHPUnit/PhpStorm 经 autoload-dev.files
PHPUintTest\PHPUintTest\Autoloader(经 PHPUintTest/register_dev_autoload.phpautoload-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 自动拉起CISWOOLEFY_HTTP_AUTO_START=1 → 后台 start,suite 结束 stop;探活失败则 fail(CI 设 SKIP_IF_DOWN=0

环境变量:

变量默认含义
SWOOLEFY_TEST_BASE_URLhttp://127.0.0.1:9501HTTP 基址
SWOOLEFY_HTTP_AUTO_START0是否自动 start/stop
SWOOLEFY_HTTP_SKIP_IF_DOWN1(本地 phpunit.xml)不可达则 skip;CI Http job 建议 0
SWOOLEFY_HTTP_READY_TIMEOUT30探活最长等待秒数
SWOOLEFY_HTTP_HITL_AUTH0PHPUnit 侧声明服务端已开 HITL(配合 WORKFLOW_HITL_AUTH_ENABLED=1
SWOOLEFY_HTTP_SHARED_RUN_STORE0声明服务端为共享 RunStore,允许跨请求 status/resume
WS_HOST / WS_PORT / WS_SMOKE_SKIP_IF_DOWN沿用现网Websocket suite

模式 B 实现要点(贴合 swoolefy CLI):

  1. 实现:HttpServerManagerSWOOLEFY_HTTP_AUTO_START=1 时执行 php cli.php restart Test --force=1 --daemon=1
  2. 探活:轮询 GET {baseUrl}/,直到可读或超时(SWOOLEFY_HTTP_READY_TIMEOUT)。
  3. 日志:stdout/stderr → Test/Storage/Logs/phpunit-http.log
  4. 收尾:仅当我们拉起的进程才 stop Test --force=1register_shutdown_function)。
  5. 入口:composer test:http:ci(已设 AUTO_START=1、SKIP_IF_DOWN=0)。
  6. 端口:固定 Test 9501;CI runner 独占。
  7. 目标环境: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)

优先级来源用例
P0OutdoorOutdoorWorkflowHttpTest:sunny / rainy / status 缺 runId
P0WorkflowWorkflowHttpTest:list;contract_review run;resume(共享 Store);status 缺 runId
P0Auth / Common ControllerPHPUintTest/Unit/Controller/*(suite=http);Redis/Cache 标 redis
P1HITL curl✅ 错 Key → 403(SWOOLEFY_HTTP_HITL_AUTH=1 + 服务端 WORKFLOW_HITL_AUTH_ENABLED=1;status 主路径;resume 另需共享 Store)
P2Order saga demoOrderWorkflowHttpTest:process mock 批准;saga → 400;status 缺 runId

引擎边角、HITL 纯逻辑仍留在 UnitWorkflowHitlAuthTest),Http 只保契约。

5.4 与直接测 Controller 的分工

Unit:  WorkflowEngine / HitlAuth / JobRunner     ← 快、密
Http:  /api/v1/outdoor/workflow/cycling         ← 少而稳(黄金路径)

禁止「所有分支都打 HTTP」。


6. 脚本 → PHPUnit 改造规程(Support)

6.1 单文件改造步骤(Checklist)

  1. 新建 PHPUintTest/Unit/Support/{Module}/{Name}Test.phpextends TestCase

  2. 将每个 function testXxx() 变为类方法;assertTrue$this->assert*

  3. 删除文件级 assertTrue / pass / 尾部 foreach

  4. Stub / Fake 类移入同文件 private class 或 PHPUintTest/Unit/Support/{Module}/Fixture/

  5. 删除旧 src/**/Tests/*Test.php 转发脚本;composer test:{module}phpunit --testsuite unit --filter X

  6. 跑通:./vendor/bin/phpunit --filter JobPhase1Test 与旧脚本结果一致后合并。

6.2 模块迁移优先级

批次模块理由
1(样板)Job Phase1、WorkflowHitlAuth无 IO、断言清晰
2Workflow Phase1–4、Integration、PluginMemorycomposer 主回归
3Agent/AI/Mcp/Neuron/Rag/Capability/DocumentOcr同模式批量
4Phase A–D、SupportLog注意部分需 Coroutine
5Websocket 离线单测 → Unit;Smoke → Websocket suite
6RedisRunStoreCas、Cluster → #[Group('redis')]
7Test/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/phpunitphpunit.xml.dist(suite 隔离);PHPUintTest/bootstrap + 三基类
P1 样板迁移JobPhase1 + WorkflowHitlAuth → PHPUintTest/Unit;旧脚本 deprecate 转发
P2 CoroutineCoroutineTestCase + Auth goApp / Context array
P3 HttpAuth /api/auth-user/me 401 + Outdoor cycling + Workflow list/run/status/resume
P4 批量 Supporttest:support 模块迁入 PHPUintTest/;composer test:* 切 phpunit
P4+ Module + Redis CASTest/Module/*PHPUintTest/Unit/ModuleRedisRunStoreCas + #[Group('redis')]
P5 WebsocketOffline + Smoke → PHPUintTest/Websocket
P5 单轨删除 deprecate wrapper;Mqtt → PHPUintTest/Unit/Mqttcomposer test:mqtt
P6HttpRequestHarness(RequestInput);composer test:http:citest: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. 验收标准

  1. 无 HTTP 服务时,composer test--testsuite unit,coroutine全部通过,且不会执行 PHPUintTest/Http
  2. 服务未启 + SWOOLEFY_HTTP_SKIP_IF_DOWN=1composer test:http skip,进程 exit 0。
  3. start Test 后 Outdoor sunny cycling:status=200 且存在 runId
  4. GET /api/auth-user/me 无 Bearer → HTTP 401;HITL 错误 API Key → 403(需 WORKFLOW_HITL_AUTH_ENABLED=1 + SWOOLEFY_HTTP_HITL_AUTH=1)。
  5. 样板模块(JobPhase1、HitlAuth、Contract Module)PHPUnit 可独立 filter 跑通。
  6. Coroutine:Auth setUser 后 goApp 子协程可读 getUserId();可选 assertCoroutineLeakWithin
  7. 本地模式 A(test:http)、CI 模式 B(test:http:ci / AUTO_START + stop)均有文档与可操作步骤。
  8. HttpRequestHarness 可构造 RequestInput(见 HttpRequestHarnessTest);不替代 HttpIntegration。

11. 风险与对策

风险对策
端口占用 / 僵尸 WorkerAUTO_START 必须配对 stop/--force;固定 9501;CI 独占 runner
group exclude 误伤 Http已定:Http 只用 suite,不 exclude @group http
双重 Coroutine\run迁入时剥掉脚本内层,只留 runInCoroutine
测试污染 DB/RedisHttp 用 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.mdcurl → Http 用例来源
docs/Auth.mdAuth / goApp / Bearer / /api/auth-user/me
docs/CapabilityTool.mdCapability 单测迁入 Unit
docs/Job.mdJob 模块与 Phase 测试说明
Test/Module/Demo 与 README curl
Test/Controller/AuthUserController.phpAuth 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 如何不进默认 CIsuite 隔离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)

  1. ✅ PHPUnit 11 + suite 隔离 + 三基类 / Http 基类
  2. ✅ Support / Module(含 Contract)/ Mqtt / Websocket 单轨 PHPUintTest/
  3. ✅ Http 黄金路径(Outdoor / Workflow / Order / Controller)+ HITL 错 Key(gated)
  4. ✅ P6:HttpRequestHarnesstest:http:citest:coverage(无强制门槛)