测试策略
merql 使用由预言结果驱动的测试模型。每个测试场景都会定义一组已知输入(三种数据库状态)、一个确定的预期结果(预言结果),并将实际合并输出与该预期结果进行比较。32 个场景涵盖无冲突合并、冲突、标识解析、数据类型、规模和边界情况。
# Run the full verification suite.
./bin/verify-all
本页中的命令需要在 merql 仓库检出后运行
composer install。bin/ 中的开发脚本、scenarios/
语料库和测试不属于 Composer 软件包。开发依赖包括 PHPUnit 13,该版本需要
PHP 8.4 或更高版本,尽管该库本身可在 PHP 8.2 上运行。
由预言结果驱动的测试
目前没有可执行数据库三方合并的参考工具,因此预言结果是根据已知输入计算得出的,而不是由外部工具生成。每个场景都有确定的预期结果,可通过逻辑验证:
- 设置 -- 创建三个内容已知的数据库状态(base、ours、theirs)。
- 预言结果 -- 根据已知输入计算预期合并结果。
- 实际结果 -- 对这三个状态运行 merql 合并。
- 比较 -- 实际结果必须与预期结果完全一致。
Known inputs (base, ours, theirs)
| |
Deterministic merql merge
computation
| |
Expected result Actual result
| |
-----> Compare <-----
|
Pass or Fail
运行测试
完整验证
verify-all 脚本是最终检查关卡。它会运行静态分析、编码规范检查、PHPUnit 测试和预言结果回归测试套件:
./bin/verify-all
在任何工作被视为完成之前,此检查都必须通过。
预言结果回归测试套件
运行全部 32 个场景:
./bin/test-regression
运行单个类别:
./bin/test-regression --category clean
./bin/test-regression --category conflict
./bin/test-regression --category identity
类别对应 scenarios/ 下的目录名称:clean、conflict、
identity、types、scale 和 edge。未知类别会显示
No scenarios found 并成功退出。可以重复使用 --category 来运行
多个类别。
脚本也接受 --jobs,但会忽略其值,并按顺序逐个运行场景(#5)。
以最少输出运行(仅通过/失败):
./bin/test-regression --fast
单个场景
通过完整流程测试一个场景:
./bin/test-scenario column-level-clean
单独运行预言结果步骤
./bin/oracle column-level-clean # Compute expected result
./bin/actual column-level-clean # Run merql, capture output
./bin/compare column-level-clean # Diff oracle vs actual
单元测试
composer test:unit # Isolated component tests
composer test:integration # Integration tests (SQLite, plus MySQL when configured)
composer test # All PHPUnit suites
composer test:oracle # Oracle regression corpus (bin/test-regression)
composer verify # analyse + cs + test + test:oracle
composer test 只运行 PHPUnit。oracle PHPUnit 套件指向
tests/Oracle,其中包含场景运行器类,但没有测试用例,因此这 32 个
场景只会通过 composer test:oracle、composer verify 或
./bin/verify-all 运行。
除非 MERQL_MYSQL_HOST、MERQL_MYSQL_DATABASE 和 MERQL_MYSQL_USER(以及可选的
MERQL_MYSQL_PASSWORD 和 MERQL_MYSQL_PORT)均指向测试数据库,否则会跳过 MySQL 集成测试。
代码质量
composer cs # PSR-12 coding standards check
composer cs:fix # Auto-fix coding standards
composer analyse # PHPStan level 10 static analysis
场景类别
clean(8 个场景)
无冲突的干净合并。双方都可以更改数据,但绝不会更改同一行的同一列。
| 场景 | 测试内容 |
|---|---|
insert-only-theirs |
对方插入行,我们这边未更改。 |
update-only-theirs |
对方更新行,我们这边未更改。 |
delete-only-theirs |
对方删除行,我们这边未更改。 |
insert-only-ours |
我们这边插入行,对方未更改。 |
mixed-no-overlap |
双方更改了不同的表或行。 |
both-same-change |
双方进行了相同的更改(无冲突)。 |
column-level-clean |
双方更改了同一行的不同列。 |
multi-table |
跨多个表的更改。 |
conflict(6 个场景)
必须产生冲突的场景。
| 场景 | 测试内容 |
|---|---|
both-update-same-column |
双方将同一列更改为不同的值。 |
update-vs-delete |
一方更新某行,另一方删除同一行。 |
delete-vs-update |
更新与删除的反向情况。 |
both-insert-same-pk |
双方都插入了主键相同的行。 |
multiple-conflicts |
一次合并中出现多个冲突。 |
partial-conflict |
部分列存在冲突,其他列可无冲突合并。 |
identity(4 个场景)
行标识边界情况。
| 场景 | 测试内容 |
|---|---|
auto-increment |
新行在各分支中具有不同的 ID。 |
natural-key |
使用唯一列而不是 PK 进行匹配。 |
composite-key |
多列主键。 |
no-key |
没有主键的表(回退到内容哈希)。 |
types(6 个场景)
涵盖所有列类型的数据类型处理。
| 场景 | 测试内容 |
|---|---|
text-columns |
VARCHAR、TEXT、LONGTEXT 值。 |
numeric-columns |
INT、DECIMAL、FLOAT 值。 |
date-columns |
DATE、DATETIME、TIMESTAMP 值。 |
json-columns |
JSON 列合并。 |
blob-columns |
二进制数据。 |
null-handling |
NULL 到值、值到 NULL 的转换。 |
scale(4 个场景)
负载下的性能。
| 场景 | 测试内容 |
|---|---|
1k-rows |
1,000 行。 |
10k-rows |
10,000 行。 |
100k-rows |
100,000 行。 |
wide-table |
含 50+ 列的表。 |
edge(4 个场景)
边界情况和边界条件。
| 场景 | 测试内容 |
|---|---|
empty-changeset |
一侧或双方都没有更改。 |
schema-mismatch |
快照之间添加或删除列。 |
encoding |
UTF-8、表情符号、特殊字符。 |
large-text |
超大的 TEXT/LONGTEXT 值。 |
场景结构
每个场景都是 scenarios/<category>/<name>/ 下的一个目录,包含 scenario.json 配置:
{
"name": "column-level-clean",
"category": "clean",
"description": "Both sides modify same row but different columns",
"tables": ["test_posts"],
"expectations": {
"conflicts": 0,
"operations": 3,
"changeset_match": "exact",
"merge_match": "exact",
"sql_match": "semantic"
}
}
对于冲突场景,预期冲突会详细列出:
{
"expectations": {
"conflicts": 1,
"conflict_details": [
{
"table": "test_posts",
"primary_key": {"id": 42},
"column": "title",
"ours_value": "Welcome",
"theirs_value": "Greetings"
}
]
}
}
ScenarioRunner 流程
ScenarioRunner::run() 负责协调单个场景:
OracleCapture::compute()根据场景的 JSON 数据构建三个快照并运行合并。ScenarioComparator::compare()根据scenario.json中的预期值检查合并结果。- 返回通过/失败状态以及失败消息列表。
use Merql\Tests\Oracle\ScenarioRunner;
$result = ScenarioRunner::run($scenario);
// ['name' => 'column-level-clean', 'pass' => true, 'failures' => []]
ScenarioRunner::runAll() 按顺序运行每个场景:
$results = ScenarioRunner::runAll($scenarios);
添加新场景
- 创建目录:
scenarios/<category>/<name>/。 - 创建
scenario.json,填写场景元数据和预期结果。 - 在 JSON 配置中定义三种数据库状态:base 数据、ours 的更改、theirs 的更改。
- 运行场景:
./bin/test-scenario <name>。 - 验证完整测试套件:
./bin/verify-all。
代码质量标准
merql 执行以下检查:
- PHPStan 10 级 -- 严格静态分析,不设基线例外。
- PSR-12 -- 由
phpcs检查编码规范。 - 预言结果回归测试 -- 32 个场景必须全部通过。
- 单元测试 -- 针对各个组件的独立测试。
verify-all 脚本会依次运行全部四项检查。不接受只通过部分检查。
持续集成
CI 会在每次向 main 推送或提交拉取请求时,运行编码规范检查、PHPStan、composer test、针对 MySQL 8.4 的 mysqli 集成测试套件、合并原语覆盖率检查,以及 composer audit。它不会运行预言结果回归语料库(#6),因此在依赖某项更改之前,请在本地运行 composer test:oracle 或 ./bin/verify-all。