Search by

teamones / testing

weijer

PHPUnit 测试基础设施,适配 teamones framework(Workerman 常驻 + ThinkPHP 3.2 派生)

1.0.0 2026-10-22 15:16 UTC

This package is auto-updated.

Last update: 2026-10-24 07:47:51 UTC


README

适配 teamones framework(Workerman 常驻 + ThinkPHP 3.2 派生) 的 PHPUnit 测试基础设施。

解决三个问题:

  1. 框架环境引导:自动定位项目根目录、加载 autoload、定义 APP_PATH / CONF_PATH / DS 等常量,测试里可以直接用框架类;
  2. 不污染运行时:只做最小初始化,不启动 Workerman、不连数据库;
  3. 安全回归断言:assertBlocked() / assertNoSqlInjection() 等,专门表达“非法输入应被拦截”。

环境要求

  • PHP >= 7.4
  • PHPUnit ^9.5(PHP 10 需要 PHP 8.1+,本项目不适用)

安装(项目内 path 仓库)

在业务项目的 composer.json 中增加:

{
    "repositories": [
        { "type": "path", "url": "packages/teamones-testing" }
    ],
    "require-dev": {
        "teamones/testing": "*"
    }
}

然后:

composer update teamones/testing

若包发布到私有 Composer 仓库,直接 composer require --dev teamones/testing 即可。

基本用法

<?php

namespace tests;

use Teamones\Testing\TestCase;

class ParseOrderTest extends TestCase
{
    public function testNormalField(): void
    {
        $ref  = new \ReflectionClass(\think\db\driver\Mysql::class);
        $db   = $ref->newInstanceWithoutConstructor();
        $m    = $ref->getMethod('parseOrder');
        $m->setAccessible(true);

        $this->assertSqlFragment(
            ' ORDER BY `id` DESC',
            fn() => $m->invoke($db, 'id DESC', false)
        );
    }

    public function testInjectionBlocked(): void
    {
        $ref  = new \ReflectionClass(\think\db\driver\Mysql::class);
        $db   = $ref->newInstanceWithoutConstructor();
        $m    = $ref->getMethod('parseOrder');
        $m->setAccessible(true);

        $this->assertBlocked(fn() => $m->invoke($db, 'id) OR 1=1 ASC', false));
    }
}

TestCase 已自动完成 Bootstrap::boot(),无需手动 require。

常用能力

方法 用途
Bootstrap::boot($root = null) 引导框架环境(幂等)
Bootstrap::detectRoot() 向上查找 vendor/autoload.php
$this->assertBlocked($fn) 断言闭包被拦截(抛异常)
$this->assertSqlFragment($expect, $fn) 断言生成的 SQL 片段
$this->assertNoSqlInjection($sql) 断言片段不含 union/sleep/outfile 等
$this->requireEnv('DB_HOST') 缺少环境变量时跳过测试
$this->assertThrows($fn) 断言抛异常并返回异常对象

在容器内运行

项目使用 Docker 部署时,直接在容器里跑,能保证 PHP 版本与生产一致:

# 包自身测试
docker exec -it teamones-saas sh -c "cd /app/packages/teamones-testing && composer install && ./vendor/bin/phpunit"

# 业务项目测试
docker exec -it teamones-saas sh -c "cd /app && ./vendor/bin/phpunit --testsuite=Application"

真实服务 HTTP 测试

单元测试覆盖不到路由、中间件、鉴权等链路。参考 webman-tech/testing 的 laravel 风格 API,本包提供 InteractsWithHttp,但不在测试进程内拉起服务——teamones 服务由 Docker/常驻进程提供,测试只负责发请求与断言。

差异对比:

项 webman-tech/testing 本包
PHP ^8.2 >= 7.4
PHPUnit ^10.5 ^9.5
HTTP 客户端 Guzzle(PSR-18) curl(无额外依赖)
服务编排 测试内启动/停止真实 webman 进程 复用已运行服务(TEST_BASE_URL)
鉴权 withToken withToken + withUserInfo()(生成 x-userinfo)

用法

use Teamones\Testing\TestCase;

class HttpSmokeTest extends TestCase
{
    protected function setUp(): void
    {
        $this->skipIfServiceUnavailable();   // 服务没起就跳过
    }

    public function testRejected(): void
    {
        $this->get('/?s=/index/%5Cthink%5Capp/invokefunction')
            ->assertCode(-404);
    }

    public function testWithAuth(): void
    {
        $this->withUserInfo(['id' => 1, 'tenant_id' => 1, 'name' => 'tester'])
            ->postJson('/project/get_project_list', [
                'param' => ['filter' => ['id' => 1], 'fields' => 'id', 'order' => 'id DESC', 'page' => [1, 1]],
            ])
            ->assertStatus(200);
    }
}

可用方法:

方法 说明
withHeader/withHeaders/withToken/withUserInfo 请求头与鉴权
get/getJson/post/postJson/putJson/deleteJson 请求
assertStatus/assertCode/assertJsonPath/assertSee/assertHeader 断言(assertCode 对应本框架的 code 业务码)
waitFor($probe, $timeout) 轮询等待副作用(借鉴 webmanWaitFor)
skipIfServiceUnavailable() 服务不可用时跳过

服务地址取环境变量 TEST_BASE_URL,默认 http://127.0.0.1:8080。

测试身份配置(X-Userinfo)

本框架的鉴权由 RequestCheck 读取 x-userinfo 头(base64 JSON)。为避免每个用例手写用户信息,包提供 UserInfoProvider:

$this->asUser('default')->postJson('/project/get_project_list', [...]);
$this->asUser('admin')->getJson('/...');
$this->withUserInfo(['id' => 1, 'tenant_id' => 1]);   // 直接指定,仍可用

配置位置(按优先级)

  1. <项目根>/tests/config/users.php
  2. <项目根>/.teones/tests/users.php(已被 .gitignore 忽略,适合放本地真实账号)
  3. 环境变量覆盖:TEST_XUSERINFO_ID、TEST_XUSERINFO_TENANT_ID、TEST_XUSERINFO_PHONE、TEST_XUSERINFO_EMAIL、TEST_XUSERINFO_NAME

配置文件格式见 tests/config/users.php.example:

return [
    'default' => ['id' => 1, 'tenant_id' => 1, 'name' => 'tester'],
    'users'   => [
        'admin' => ['id' => 2, 'tenant_id' => 1, 'name' => 'admin'],
    ],
];

注意

  • id 与 tenant_id 必填;缺失时 asUser() 会抛异常并给出配置路径提示,不会静默使用假身份;
  • 服务端会按 id 同步用户(可能写库),请填测试环境的真实账号,不要编造;
  • CI 建议用环境变量注入,避免把账号写进仓库;
  • 配置文件含手机号/邮箱等个人信息,不要提交到版本库。

注意事项

  1. 不要依赖 HTTP 请求对象:常驻框架的 request() 依赖 Workerman 连接,单元测试里不可用;请把待测逻辑抽成纯方法或直接用反射调用;
  2. 不要连业务库:涉及 DB 的测试请单独配置测试库,并用 requireEnv() 做保护;
  3. 全局状态:C() 配置、静态属性在同进程内会残留,测试间注意隔离(必要时在每个用例里重置);
  4. phpunit.xml 中的 convertErrorsToExceptions 等旧属性在新版 PHPUnit 中会告警,按需升级。