Search by

yicmf / tools

yicmf

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.

Package info

gitee.com/yicmf/tools

Type:think-extend

pkg:composer/yicmf/tools

Statistics

Installs: 66

Dependents: 1

Suggesters: 0

1.1.0 2026-07-31 06:04 UTC

This package is auto-updated.

Last update: 2026-10-01 15:45:13 UTC


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%:

  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/ 的自动加载器,否则 <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,不触网):

驱动MethodsLines纯计算用例HTTP 交互用例
Oss64.71%84.92%tests/OssTest.phptests/OssMultipartTest.php
Cos52.63%86.36%tests/CosTest.phptests/CosMultipartTest.php
Qiniu57.14%81.97%tests/QiniuTest.phptests/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 会拒绝启动)。