コンテンツへスキップ

テスト戦略

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方向のデータベースマージを行う既存の参照ツールはないため、オラクルは外部ツールで生成するのではなく、既知の入力から算出します。各シナリオには、ロジックで検証できる決定的な期待結果があります。

  1. セットアップ -- 既知の内容で3つのデータベース状態(base、ours、theirs)を作成します。
  2. オラクル -- 既知の入力から期待されるマージ結果を算出します。
  3. 実行結果 -- merqlで3つの状態をマージします。
  4. 比較 -- 実際の結果が期待結果と完全に一致することを確認します。
コード
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設定ファイルを含みます。

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"
    }
}

競合シナリオでは、期待される競合を詳細に指定します。

JSON
{
    "expectations": {
        "conflicts": 1,
        "conflict_details": [
            {
                "table": "test_posts",
                "primary_key": {"id": 42},
                "column": "title",
                "ours_value": "Welcome",
                "theirs_value": "Greetings"
            }
        ]
    }
}

ScenarioRunnerのパイプライン

ScenarioRunner::run()は単一のシナリオを処理します。

  1. OracleCapture::compute()は、シナリオのJSONデータから3つのスナップショットを作成し、マージを実行します。
  2. ScenarioComparator::compare()は、scenario.jsonの期待値とマージ結果を照合します。
  3. 成功または失敗と、失敗メッセージの一覧を返します。
PHP
use Merql\Tests\Oracle\ScenarioRunner;

$result = ScenarioRunner::run($scenario);
// ['name' => 'column-level-clean', 'pass' => true, 'failures' => []]

ScenarioRunner::runAll()はすべてのシナリオを順番に実行します。

PHP
$results = ScenarioRunner::runAll($scenarios);

新しいシナリオの追加

  1. ディレクトリを作成します:scenarios/<category>/<name>/。
  2. シナリオのメタデータと期待値を記述したscenario.jsonを作成します。
  3. JSON設定で3つのデータベース状態を定義します:baseデータ、oursの変更、theirsの変更。
  4. シナリオを実行します:./bin/test-scenario <name>。
  5. テストスイート全体を検証します:./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を実行してください。