yicmf / tools
tools
This package's canonical repository appears to be gone and the package has been frozen as a result. Email us for help if needed.
Requires
- php: >=7.0
- overtrue/pinyin: ~3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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):
# 项目根目录执行
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(两套配置并存、互不影响):
# 在包根目录执行;phpunit 二进制取宿主项目的(本包目录内没有可用的 vendor/bin)
php -d pcov.directory="<本包绝对路径>/src" \
"<项目绝对路径>/vendor/phpunit/phpunit/phpunit" \
-c phpunit.coverage.xml --coverage-text
# 生成可浏览的 HTML 报告(输出到 outputs/coverage_html/)
php -d pcov.directory="<本包绝对路径>/src" \
"<项目绝对路径>/vendor/phpunit/phpunit/phpunit" \
-c phpunit.coverage.xml --coverage-html outputs/coverage_html
⚠️ 两个必须,漏一个覆盖率就是 0%:
pcov.directory必须显式传 —— pcov 默认只采样自己pcov.directory下的文件, 不传就采不到,--coverage-text会打出Lines: 0.00% (0/6949)。- 必须用
tests/bootstrap-coverage.php—— PHPUnit 从宿主项目 vendor 启动时, 项目 autoload 会把yicmf\tools\解析到vendor/yicmf/tools/src/;该引导前置注册了 指向本包src/的自动加载器,否则<source>指到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():
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 把它抽成工厂方法:
// src/AlipayService.php
protected function http(): HttpClient
{
return HttpClient::instance();
}
// 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:"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 会拒绝启动)。