Files
ulthon_admin/tests/README.md
augushong 37cb8291f8 feat(test): 引入测试规则文档与参数化测试库初始化脚本
对齐 wkbox 衍生项目的测试规则体系,剥离业务特定内容后移植到框架:

- 规则 .agents/rules/ulthon-testing.md(设计哲学/决策树/4层模型/安全规范/fixture原则)
- 技能 .agents/skills/ulthon-testing(运行/编写/模板/决策/回归验证)
- 参数化建库脚本 tests/setup_test_db.{php,ps1,sh} + README
- AGENTS.md 索引补充

与 wkbox 差异:测试库参数化(复用 .env 连接,database=env+'_test'),不硬编码 Docker;删去 canBind Oracle 业务约束与具体 fixture 工厂,业务工厂由衍生项目自行实现。
2026-07-18 23:56:32 +08:00

177 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 测试数据库初始化
本目录提供测试数据库(默认 `ulthon_admin_test`)的初始化脚本。运行一次即可在与 `.env`
同实例的 MySQL 上建出框架运行所需的**全部表**(系统表 + 业务表),并插入最小参考数据。
## 测试库策略:参数化,无需专用 Docker 容器
ulthon 是框架仓库,测试库策略**与衍生项目wkbox不同**
- **完全参数化**连接参数hostname / hostport / username / password / charset /
prefix / database全部从 `.env` 读取,复用开发库同实例的 MySQL。
- **不依赖 Docker**:单机装一个 MySQL 即可,不需要为测试专起容器。
- **测试库名派生规则**`.env``DATABASE` + `_test` 后缀。如 `DATABASE=ulthon_admin`
则测试库为 `ulthon_admin_test`;若 `.env` 改成别的项目名,测试库名会跟着变。
- **脚本自带建库**:用 PDO 直连 MySQL 执行 `CREATE DATABASE IF NOT EXISTS`,测试库
不存在会自动创建utf8mb4 / utf8mb4_unicode_ci已存在则幂等跳过。
> 衍生项目(如 wkbox如果偏好专用 Docker 容器做物理隔离,可以 fork 本脚本,把 PDO
> 建库段换成 `docker exec ... mysql -e "..."`,并把连接参数改成硬编码容器端口——
> 但 ulthon 框架自身保持参数化、零容器假设。
## 文件说明
| 文件 | 作用 |
|------|------|
| `setup_test_db.php` | 真正的运行器PHP。启动 ThinkPHP 容器、读 `.env`、PDO 建测试库、config 级覆盖连接、按序调用 `migrate:run` / `scheme:sync` / `seed:run`、查 `information_schema` 自验证。 |
| `setup_test_db.ps1` | Windows / PowerShell 包装器:检测 php 在 PATH → 调 PHP 运行器 → 透传退出码。 |
| `setup_test_db.sh` | bash / CI 包装器逻辑同上Linux / macOS / CI 用。 |
| `bootstrap.php` | PHPUnit 引导脚本(已存在,独立维护)。连接覆盖逻辑与本脚本完全一致,二者必须保持同步——否则 PHPUnit 跑的测试库与本脚本初始化的不是同一个。 |
## 如何运行
```powershell
# Windows / PowerShell
powershell -File tests/setup_test_db.ps1
# 或直接跑运行器
php tests/setup_test_db.php
```
```bash
# Linux / macOS / CI
bash tests/setup_test_db.sh
# 或
php tests/setup_test_db.php
```
成功后测试库(如 `ulthon_admin_test`)会包含框架所需的系统表(`ul_system_*`)以及
`app/admin/scheme/` 下声明的业务表,退出码为 `0`。脚本自带验证段,会打印表总数并按
`system` / `app_` / 其它分类,并断言关键系统表存在。
## 关键概念migrate:run 与 scheme:sync 两套机制,缺一不可
ulthon 的建表有**两个独立的来源**,二者不能互相替代:
| 机制 | 命令 | 来源目录 | 建什么表 | 举例 |
|------|------|----------|----------|------|
| Phinx 迁移 | `php think migrate:run` | `database/migrations/` | 系统 / 框架基础表 | `ul_system_admin``ul_system_menu``ul_system_auth_node``ul_system_config``ul_system_host``ul_system_timer_*``ul_debug_log` 等 |
| Scheme 同步 | `php think scheme:sync` | `app/admin/scheme/` | 业务表(用 PHP 8 Attribute 声明) | 框架仓库本身不带业务表,使用方按需在 `app/admin/scheme/` 添加 |
- **只跑 `migrate:run`**:得到系统表,但没有业务表。
- **只跑 `scheme:sync`**:得到业务表,但没有系统表,框架无法登录、无法鉴权。
- **必须两个都跑**,业务表通常引用系统表(如外键关联 `system_admin`)。
`seed:run` 负责最小参考数据(超级管理员、权限节点、系统设置、菜单、快捷入口等),由
`database/seeds/InitBaseAdminData.php` 提供,自带 install-lock重复执行安全跳过。
本脚本在**同一进程**内依次调用这三条命令(见下节),确保它们都指向测试库。
## 技术细节:为什么用 config 级覆盖,而不是环境变量
ThinkPHP 的 `Env` 组件加载 `.env` 文件,且 **`.env` 的值优先于 OS 环境变量**
`getenv` 仅作为 `.env` 中不存在键的 fallback。因此下列方式都**无法**覆盖 `.env`
里已存在的连接键(`HOSTNAME` / `DATABASE` / `USERNAME` / `PASSWORD` / `HOSTPORT` 都在
`.env` 中):
- `putenv('DATABASE=ulthon_admin_test')`PHP
- `$env:DATABASE='ulthon_admin_test'`PowerShell
- `DATABASE=ulthon_admin_test php think migrate:run`bash 前缀变量)
**唯一可靠**的覆盖点是 **config 级**:在 `App::initialize()` 之后,直接改写
`config/database.php` 解析出的连接配置数组。因为 `Db` facade 运行时读 config
config 被改写后,后续所有 DB 操作(包括 `migrate:run` / `scheme:sync` / `seed:run`
内部)都连到测试库。
`setup_test_db.php` 的核心步骤:
```php
require __DIR__ . '/../vendor/autoload.php';
$app = new \think\App();
$app->initialize();
// 从 .env 读连接参数(与 bootstrap.php 完全一致)
$hostname = \think\facade\Env::get('database.hostname');
// ...其它参数同理
$testDatabase = \think\facade\Env::get('database.database') . '_test';
// PDO 直连(不指定 database建测试库
$pdo = new \PDO("mysql:host={$hostname};port={$hostport};charset={$charset}", $username, $password);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$testDatabase}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
// 把整段 'database' 配置 pull 出来,原地改 connections.main再 set 回去。
// 注意think\Config::set(array, $name) 的 $name 只支持单段一级名,
// 不像 Config::get 那样解析点号多级路径——不能直接传 'database.connections.main'。
$dbConfig = \think\facade\Config::get('database');
$dbConfig['default'] = 'main';
$dbConfig['connections']['main'] = array_merge(
$dbConfig['connections']['main'] ?? [],
[
'hostname' => $hostname, 'hostport' => $hostport,
'database' => $testDatabase, 'username' => $username, 'password' => $password,
'charset' => $charset, 'prefix' => $prefix, 'fields_cache' => false,
],
['query' => \app\common\provider\db\Query::class], // 必须保留框架自定义查询类
);
\think\facade\Config::set($dbConfig, 'database');
// 安全护栏:库名必须含 'test' 才允许继续
if (stripos(\think\facade\Config::get('database.connections.main.database'), 'test') === false) {
throw new \RuntimeException('拒绝执行:当前库不像测试库');
}
// 同进程内调用 think 命令config 覆盖对它们同样生效
\think\facade\Console::call('migrate:run', ['--force-force']);
\think\facade\Console::call('scheme:sync', ['--force-force']); // --force-force 跳过交互确认
\think\facade\Console::call('seed:run', ['--force-force']);
```
### 关于 `--force-force``-ff`
`scheme:sync` 在检测到 Scheme 与 DB 有差异时会**交互式确认**"确认要将这些变更应用到
数据库吗?")。非 TTY 环境脚本、CI`think\console\Output::confirm` 会抛
"Aborted" 而失败。框架提供了全局 `--force-force` / `-ff` 选项(见
`extend/base/common/provider/ConsoleBase.php``extend/base/common/console/OutputBase.php`
命中时 `confirm()` 直接返回默认值,跳过所有交互确认。本脚本对三条命令统一带上该参数,
保证非交互可运行、可重跑。
### 关于默认连接名 `main`
本项目 `config/database.php` 的默认连接是 `main`(不是 ThinkPHP 常见的 `mysql`
`default = Env::get('database.main')``.env``MAIN=main`。因此覆盖目标是
`connections.main`**不是** `connections.mysql`
## 幂等性
脚本可安全重复执行:
- `migrate:run`:已执行的迁移记录在 `ul_migrations`phinxlog二次运行直接 "All Done" 无操作。
- `scheme:sync`:无差异时打印 "未检测到 Scheme 变更";有差异时先备份原表(`ul_backup_<时间戳>_<表名>`)再改,不报错。
- `seed:run``InitBaseAdminData` 自带 install-lock`base_admin_install` 配置项),二次运行打印 "系统已初始化,跳过当前程序"。
- `CREATE DATABASE IF NOT EXISTS`:库已存在时无操作。
> 注:`scheme:sync` 每次遇到差异都会生成 `ul_backup_*` 备份表,这是框架的设计行为
> (改表结构前自动备份)。若测试库中积累了大量备份表,可直接 `DROP DATABASE` +
> `CREATE DATABASE` 后重跑本脚本重建(见下)。
## 彻底重置测试库
当测试库状态污染、备份表堆积、或想从干净状态重新开始时:
```bash
# 用 .env 里的 root 账号连本机 MySQL按实际 .env 替换 host/port/user/pass
mysql -h127.0.0.1 -P3306 -uroot -proot \
-e "DROP DATABASE IF EXISTS ulthon_admin_test; \
CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 然后重跑初始化
php tests/setup_test_db.php
```
PowerShell 等价:
```powershell
mysql -h127.0.0.1 -P3306 -uroot -proot `
-e "DROP DATABASE IF EXISTS ulthon_admin_test; `
CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
php tests/setup_test_db.php
```