# tools **Repository Path**: yicmf/tools ## Basic Information - **Project Name**: tools - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-07-25 - **Last Updated**: 2026-10-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # tools ThinkPHP 通用工具扩展包(HTTP / 支付 / 上传 / 缓存锁 / 加密 / 第三方服务封装)。 ## 单元测试约定 包内所有单元测试**统一使用 PHPUnit**,用例类放在 `tests/` 目录(`*Test.php`,命名空间 `yicmf\tools\tests`,统一继承 `yicmf\tools\tests\TestCase` 基类)。 运行方式(用带 bcmath / openssl / mbstring 等扩展的 PHP,如项目的 `D:\phpEnv\php\php-8.3\php.exe`): ```bash # 项目根目录执行 php vendor/bin/phpunit -c vendor/yicmf/tools/phpunit.xml # 仅跑依赖外网的真实请求用例(默认排除) php vendor/bin/phpunit -c vendor/yicmf/tools/phpunit.xml --group network ``` 说明: - `tests/bootstrap.php` 负责引导:优先加载宿主项目 composer autoload,并在 PHPUnit 安装错误处理器之前完成 think 框架初始化(`FlowSn` / `RedisLock` / `UploadService` 等依赖框架的用例通过 `requireFramework()` 引导,失败时自动跳过)。 - 基类 `TestCase` 提供 protected 成员反射访问(`invokeProtected` / `getProtected` / `setProtected`)、`assertThrows()` 异常断言与框架引导。 - 依赖外网真实请求的用例标注 `@group network`,默认在 phpunit.xml 中排除。 - 依赖特定扩展(bcmath / openssl / mbstring)或外部服务的用例在 `setUp()` 中检测并 `markTestSkipped()`。 - 不再新增脚本式断言测试(自写 assert + exit code),一律走 PHPUnit。 ## 代码覆盖率(需 ext-pcov 或 ext-xdebug) 日常回归不采集覆盖率;需要覆盖率时用**单独的** `phpunit.coverage.xml`(两套配置并存、互不影响): ```bash # 在包根目录执行;phpunit 二进制取宿主项目的(本包目录内没有可用的 vendor/bin) php -d pcov.directory="<本包绝对路径>/src" \ "<项目绝对路径>/vendor/phpunit/phpunit/phpunit" \ -c phpunit.coverage.xml --coverage-text ``` ```bash # 生成可浏览的 HTML 报告(输出到 outputs/coverage_html/) php -d pcov.directory="<本包绝对路径>/src" \ "<项目绝对路径>/vendor/phpunit/phpunit/phpunit" \ -c phpunit.coverage.xml --coverage-html outputs/coverage_html ``` ⚠️ 两个**必须**,漏一个覆盖率就是 0%: 1. **`pcov.directory` 必须显式传** —— pcov 默认只采样自己 `pcov.directory` 下的文件, 不传就采不到,`--coverage-text` 会打出 `Lines: 0.00% (0/6949)`。 2. **必须用 `tests/bootstrap-coverage.php`** —— PHPUnit 从宿主项目 vendor 启动时, 项目 autoload 会把 `yicmf\tools\` 解析到 `vendor/yicmf/tools/src/`;该引导**前置**注册了 指向本包 `src/` 的自动加载器,否则 `` 指到 `src/` 也采不到行。 基线(2026-09-29,858 tests):Classes 20.59%、Methods 46.00%、**Lines 60.42%**; `QrCode` 为 Methods 67.16% / **Lines 88.36%**,`AlipayService` 为 Methods 88.37% / **Lines 98.22%**。 覆盖率低的大多是依赖外网 / 微信 token 的类。 云存储三驱动(`upload\storage\`)已覆盖签名计算 **与** HTTP 交互全链路(离线 mock,不触网): | 驱动 | Methods | Lines | 纯计算用例 | HTTP 交互用例 | |------|---------|-------|-----------|--------------| | `Oss` | 64.71% | **84.92%** | `tests/OssTest.php` | `tests/OssMultipartTest.php` | | `Cos` | 52.63% | **86.36%** | `tests/CosTest.php` | `tests/CosMultipartTest.php` | | `Qiniu` | 57.14% | **81.97%** | `tests/QiniuTest.php` | `tests/QiniuMultipartTest.php` | 支付宝网关同样走离线 mock(`tests/AlipayExecuteTest.php`,88 用例):请求装配、签名覆盖 「系统参数 ∪ 业务参数」全集、响应解析(业务节点 / `error_response`)、**响应验签**、 网络异常包装,以及 15 个业务接口的 biz 参数装配。响应验签用**运行时生成的自签证书**闭环 (同一对 RSA 密钥既当应用私钥、又当支付宝公钥证书),**不内嵌任何真实支付宝密钥材料**。 ## HTTP 交互测试(离线驱动 Guzzle) `tests/HttpMockTestCase.php` 提供 `MockHttpClient`——构造时把 Guzzle Client 换成 `MockHandler` 驱动的实现,从而在**完全离线**下跑通驱动的真实请求装配 (签名 → header → URL → body)并按序返回预设响应,可断言: 请求 method/URI/header/body、4xx 错误解析、分片状态机、失败自动 abort 等。 测试内的驱动子类只需重写 `http()` 并把 `HttpClient::instance()` 换成 `new MockHttpClient()`: ```php class TestableOss extends Oss { protected function http(): HttpClient { // ⚠️ 必须保留原有 setter 链(timeout / http_errors),否则与生产行为不一致 return (new MockHttpClient())->setTimeout(30)->setHttpErrors(false); } } ``` ⚠️ **关键坑**:Guzzle 的 `HandlerStack::create()` 默认推入 `Middleware::httpErrors()`, 它读取的是**请求 option** 里的 `http_errors`(由 `HttpClient::_getOptions()` 从 config 传入)。 若在 mock 子类里丢掉 `setHttpErrors(false)`,仅靠 `new Client(['http_errors' => false])` 的构造参数会被请求级 option 覆盖 → 4xx 直接抛 `RequestException`, 驱动的 `parseError()` / `assertHttpStatus()` 分支永远不可达,测试结论失真。 ### `http()` 工厂方法(子类可重写的 HTTP 装配点) 上例能成立的前提是**驱动把 HTTP 客户端装配收在 `protected function http()` 里**。 若某类的请求是内联的 `HttpClient::instance()->...`(如 `AlipayService::execute()`), 子类无从替换,只能改 src 把它抽成工厂方法: ```php // src/AlipayService.php protected function http(): HttpClient { return HttpClient::instance(); } ``` ```php // tests/AlipayExecuteTest.php class TestableAlipay extends AlipayService { protected function http(): HttpClient { // 此处**不带** setter 链:timeout / http_errors 由 execute() 内的链式调用设置, // 与驱动类不同(驱动的装配写在 http() 内,故需在 http() 里保留 setter)。 return new MockHttpClient(); } } ``` > 判断标准:**setter 链在哪写成,就在哪保留**。`execute()` 内链式调用的,`http()` 只需返回客户端; > 驱动 `http()` 内自己链式设置的,`http()` 必须原样保留,否则测试与生产不等价。 ⚠️ 断言某类的 `http()` **生产实现**时,必须用**该类自己的实例**(而非测试子类), 否则调用的是子类重写版,父类实现永远不会被执行到(覆盖率上体现为该方法始终未覆盖)。 > 实测环境补充(2026-09-29):phpEnv 的 php.ini 内 xdebug 默认是注释状态(`;zend_extension = xdebug`), > 且 pcov 未安装。用 phpEnv 的 PHP 跑覆盖率时可用命令行动态挂载,无需改 php.ini: > > ```bash > "D:/phpEnv/php/php-8.3/php.exe" -d zend_extension=php_xdebug.dll -d xdebug.mode=coverage \ > "<项目>/vendor/phpunit/phpunit/phpunit" -c phpunit.coverage.xml --coverage-text > ``` > > 注意别用宿主 WinGet 安装的 php(可能缺 mbstring,PHPUnit 会拒绝启动)。