테스트 전략
merql은 오라클 기반 테스트 모델을 사용합니다. 각 테스트 시나리오는 입력값(데이터베이스 상태 세 가지)과 결정론적인 예상 결과(오라클)를 정의하고, 실제 병합 결과를 예상 결과와 비교합니다. 32개 시나리오에서 정상 병합, 충돌, 식별성 확인, 데이터 유형, 규모, 경계 사례를 다룹니다.
# Run the full verification suite.
./bin/verify-all
이 페이지의 명령은 merql 저장소를 체크아웃한 상태에서 composer install한 뒤 실행합니다. bin/의 개발 스크립트, scenarios/ 말뭉치, 테스트는 Composer 패키지에 포함되지 않습니다. 개발 의존성에는 PHPUnit 13이 포함되며, 이 버전은 PHP 8.4 이상이 필요합니다. 라이브러리 자체는 PHP 8.2에서 실행됩니다.
오라클 기반 테스트
3방향 데이터베이스 병합을 수행하는 기존 참조 도구가 없으므로, 외부 도구로 결과를 생성하는 대신 알려진 입력에서 오라클을 계산합니다. 각 시나리오에는 논리적으로 검증할 수 있는 결정론적 예상 결과가 있습니다:
- 설정 -- 알려진 콘텐츠로 세 가지 데이터베이스 상태(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을 통해서만 실행됩니다.
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 |
한 번의 병합에서 여러 충돌 발생. |
partial-conflict |
일부 열은 충돌하고 나머지는 충돌 없이 병합됨. |
identity (시나리오 4개)
행 식별의 경계 사례입니다.
| 시나리오 | 테스트 |
|---|---|
auto-increment |
새 행의 ID가 각 브랜치에서 서로 다름. |
natural-key |
기본 키 대신 고유 열로 일치시킴. |
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, mysqli를 사용하는 MySQL 8.4 대상 통합 테스트, 병합 기본 연산 커버리지, composer audit을 실행합니다. 오라클 회귀 테스트 말뭉치(#6)는 실행하지 않으므로, 변경 사항을 신뢰하기 전에 로컬에서 composer test:oracle 또는 ./bin/verify-all을 실행하세요.