chore(test): 引入 PHPUnit 测试基建(依赖+配置+bootstrap)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
augushong
2026-07-18 00:12:30 +08:00
parent 1ca1f86150
commit 44621b1e9d
5 changed files with 1907 additions and 2 deletions

View File

@@ -35,7 +35,8 @@
"psr/simple-cache": ">=1.0"
},
"require-dev": {
"symfony/var-dumper": "*"
"symfony/var-dumper": "*",
"phpunit/phpunit": "10"
},
"autoload": {
"psr-4": {

1786
composer.lock generated

File diff suppressed because it is too large Load Diff

14
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
stopOnFailure="false"
cacheDirectory="tests/.phpunit.cache"
>
<testsuites>
<testsuite name="feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
</phpunit>

0
tests/Feature/.gitkeep Normal file
View File

106
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,106 @@
<?php
/**
* PHPUnit 引导脚本.
*
* 职责:
* 1. require composer autoload
* 2. 引导 ThinkPHP 容器App::initialize
* 3. 【config 级覆盖】把默认连接 connections.main 指向测试库
* 4. 早期安全护栏:解析出的库名必须含 "test",否则拒绝运行
*
* 与 wkbox 衍生项目同构,但区别:
* - 连接参数从 .env 读取hostname/hostport/username/password/charset/prefix
* 复用开发库同实例的连接(典型单机 mysql测试库仅库名不同
* - 测试库名 = .env 中 DATABASE 值 + "_test" 后缀(如 ulthon_admin → ulthon_admin_test
*
* 为什么用 config 级覆盖而不是环境变量:
* ThinkPHP 的 Env 组件加载 .env且 .env 值【优先于】OS 环境变量getenv 只是 fallback
* .env 里已存在 HOSTNAME/DATABASE/USERNAME/PASSWORD/HOSTPORTputenv/$env:VAR/bash 前缀变量
* 都无法覆盖它们。唯一可靠覆盖点 = config 级App::initialize() 之后直接改写
* config/database.php 解析出的连接数组。Db facade 运行时读 config覆盖后所有 DB
* 操作都连测试库。
*
* 关键坑点think 8
* think\Config::set(array $config, ?string $name) 的 $name 只支持【单段一级配置名】,
* 不像 Config::get 那样解析点号多级路径。直接传 'database.connections.main' 会被当成
* 带点的字面顶层 key根本没写进嵌套位置静默失败
* 正确做法:把整段 'database' 配置 pull 出来 → 原地改 connections.main → set 回 'database'。
*
* 不在此做的事:
* - 不 require 全局 common.phpApp::initialize() + autoload 已够引导 facade
* helper.php 全局函数测试基建用不到)
* - 不开事务(事务隔离由 TestCaseBase.setUp 负责)
* - 不跑 migrate/scheme/seed由独立的 setup 脚本负责)
* - 不销毁容器容器跨测试复用tearDown 只回滚事务)
*/
declare(strict_types=1);
// ----------------------------------------------------------------------------
// 1. autoload + ThinkPHP 容器引导
// ----------------------------------------------------------------------------
require __DIR__ . '/../vendor/autoload.php';
$app = new \think\App();
$app->initialize();
// ----------------------------------------------------------------------------
// 2. 从 .env 读取连接参数,组装测试库连接配置
// 复用开发库同实例的连接参数hostname/hostport/username/password/charset/prefix
// 仅将 database 名替换为测试库名({原库名}_test
// ----------------------------------------------------------------------------
$envHostname = \think\facade\Env::get('database.hostname');
$envHostport = \think\facade\Env::get('database.hostport');
$envUsername = \think\facade\Env::get('database.username');
$envPassword = \think\facade\Env::get('database.password');
$envCharset = \think\facade\Env::get('database.charset');
$envPrefix = \think\facade\Env::get('database.prefix');
$envDatabase = \think\facade\Env::get('database.database');
// 测试库名策略:原库名 + _test 后缀(如 ulthon_admin → ulthon_admin_test
$testDatabase = is_string($envDatabase) && $envDatabase !== ''
? $envDatabase . '_test'
: 'ulthon_admin_test';
// ----------------------------------------------------------------------------
// 3. config 级覆盖 connections.main 指向测试库
// 覆盖目标 = connections.main本项目 config/database.php 默认连接是 main
// 保留框架自定义 query 类与其它运行时选项,只改"指向哪个库"的部分。
// ----------------------------------------------------------------------------
$dbConfig = \think\facade\Config::get('database');
if (!is_array($dbConfig)) {
$dbConfig = [];
}
$dbConfig['default'] = 'main';
if (!isset($dbConfig['connections']) || !is_array($dbConfig['connections'])) {
$dbConfig['connections'] = [];
}
if (!isset($dbConfig['connections']['main']) || !is_array($dbConfig['connections']['main'])) {
$dbConfig['connections']['main'] = [];
}
$dbConfig['connections']['main'] = array_merge(
$dbConfig['connections']['main'],
[
'type' => 'mysql',
'hostname' => is_string($envHostname) && $envHostname !== '' ? $envHostname : '127.0.0.1',
'hostport' => is_string($envHostport) && $envHostport !== '' ? $envHostport : '3306',
'database' => $testDatabase,
'username' => is_string($envUsername) && $envUsername !== '' ? $envUsername : 'root',
'password' => is_string($envPassword) ? $envPassword : '',
'charset' => is_string($envCharset) && $envCharset !== '' ? $envCharset : 'utf8mb4',
'prefix' => is_string($envPrefix) && $envPrefix !== '' ? $envPrefix : 'ul_',
'fields_cache' => false,
],
// 必须保留框架自定义 query 类,否则 ORM 行为异常。
['query' => \app\common\provider\db\Query::class],
);
\think\facade\Config::set($dbConfig, 'database');
// ----------------------------------------------------------------------------
// 4. 早期安全护栏:在任何测试运行前,确认解析出的库名含 "test"
// TestCaseBase.setUp 会再校验一次;这里更早触发,防患于未然)
// ----------------------------------------------------------------------------
$resolvedDb = \think\facade\Config::get('database.connections.main.database');
if (!is_string($resolvedDb) || stripos($resolvedDb, 'test') === false) {
throw new \RuntimeException('Test bootstrap refusing non-test database: ' . $resolvedDb);
}