テスト戦略
merqlはオラクル駆動のテストモデルを採用しています。各テストシナリオでは、既知の入力(3つのデータベース状態)と決定的な期待結果(オラクル)を定義し、実際のマージ出力と期待結果を比較します。32のシナリオで、競合のないマージ、競合、識別情報の解決、データ型、規模、エッジケースを網羅します。
# Run the full verification suite.
./bin/verify-all
このページのコマンドは、merqlリポジトリをチェックアウトし、composer installを実行した状態で使用します。bin/の開発用スクリプト、scenarios/コーパス、テストはComposerパッケージに含まれません。開発依存関係にはPHPUnit 13が含まれ、PHP 8.4以降が必要ですが、ライブラリ自体はPHP 8.2で動作します。
オラクル駆動テスト
3方向のデータベースマージを行う既存の参照ツールはないため、オラクルは外部ツールで生成するのではなく、既知の入力から算出します。各シナリオには、ロジックで検証できる決定的な期待結果があります。
- セットアップ -- 既知の内容で3つのデータベース状態(base、ours、theirs)を作成します。
- オラクル -- 既知の入力から期待されるマージ結果を算出します。
- 実行結果 -- merqlで3つの状態をマージします。
- 比較 -- 実際の結果が期待結果と完全に一致することを確認します。
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
カテゴリを1つ指定して実行します。
./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も受け付けますが、値は無視し、シナリオを1つずつ実行します(#5)。
出力を最小限にして実行します(成功/失敗のみ)。
./bin/test-regression --fast
単一シナリオ
全工程を通して1つのシナリオをテストします。
./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を通じてのみ実行されます。
MySQL統合テストは、MERQL_MYSQL_HOST、MERQL_MYSQL_DATABASE、MERQL_MYSQL_USER(および任意でMERQL_MYSQL_PASSWORDとMERQL_MYSQL_PORT)がテストデータベースを指している場合にのみ実行されます。
コード品質
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 |
theirsで行を挿入し、oursでは変更なし。 |
update-only-theirs |
theirsで行を更新し、oursでは変更なし。 |
delete-only-theirs |
theirsで行を削除し、oursでは変更なし。 |
insert-only-ours |
oursで行を挿入し、theirsでは変更なし。 |
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 |
1回のマージで複数の競合が発生。 |
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データから3つのスナップショットを作成し、マージを実行します。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設定で3つのデータベース状態を定義します:baseデータ、oursの変更、theirsの変更。
- シナリオを実行します:
./bin/test-scenario <name>。 - テストスイート全体を検証します:
./bin/verify-all。
コード品質基準
merqlでは次の基準を適用します。
- PHPStanレベル10 -- 例外のベースラインなしで厳密に静的解析します。
- PSR-12 --
phpcsでコーディング規約を確認します。 - オラクル回帰テスト -- 32のシナリオすべてに合格する必要があります。
- 単体テスト -- 各コンポーネントを個別にテストします。
verify-allスクリプトは4つのチェックを順番に実行します。一部のみの承認は認められません。
継続的インテグレーション
CIは、mainへの各プッシュとプルリクエストで、コーディング規約、PHPStan、composer test、mysqli経由でMySQL 8.4に対する統合テスト、マージプリミティブのカバレッジ、composer auditを実行します。オラクル回帰コーパス(#6)は実行しないため、変更を信頼する前に、ローカルでcomposer test:oracleまたは./bin/verify-allを実行してください。