摘要
Day 14 已經把契約 v1.0 / v1.1 的基本比較骨架接起來,證明同一份 Bundle 可以因為 v1.1 新增 UCUM system/code 要求而得到不同 Gate 結果。Day 15 把代表案例整理成可重現的最小情境測試包,讓後續開始有 Expected / Actual 形式的回歸測試骨架。
Day 14 已經可以回答:
同一份 Bundle 在契約 v1.0 與 v1.1 下,Gate 結果會不會不同?
但如果只有一個 UCUM system 錯誤案例,還不夠支撐完整的情境測試包。
第 3 週需要開始回答另一個問題:
這套 Quality Gate 能不能用一組可重跑案例,穩定說明不同資料品質情境?
所以 Day 15 的重點不是新增更多規則,而是把測試案例整理成幾種可解釋的情境:
兩版都通過
v1.1 才阻擋
兩版都阻擋
規則不適用 NOT_APPLICABLE
這樣之後文章在說明「版本差異」時,才不會把所有錯誤都誤解成契約升級造成。
今天新增或修改的範圍有:
ContractScenarioCaseTests
non-quantity-observation-bundle.json
20260816.md
今天的成果仍然集中在 test 與 fixture 層。
首頁仍維持 Day 13 / Day 14 的單一版本驗證畫面。
Day 14 的比較測試是單一情境:
observation-quantity-wrong-ucum-system.json
v1.0 → PASSED
v1.1 → BLOCKED
這個測試很適合證明 ContractComparisonService 的骨架成立。
但如果要寫第 3 週的「情境測試包設計」,就需要把案例拆成不同目的。
Day 15 先鎖定四個最小代表案例:
| 案例 | fixture | v1.0 | v1.1 | 重點 |
|---|---|---|---|---|
| 合法最小檢驗 Bundle | valid-minimal-lab-bundle.json |
PASSED | PASSED | 正常資料不受版本比較影響 |
| v1.1 新增 UCUM 要求 | observation-quantity-wrong-ucum-system.json |
PASSED | BLOCKED | 差異只來自 LAB-UNIT-002 |
| Patient reference 不存在 | missing-internal-reference.json |
BLOCKED | BLOCKED | 觸發 LAB-REF-001 與 LAB-REF-003 |
| 非 Quantity Observation | non-quantity-observation-bundle.json |
PASSED | PASSED | 覆蓋 unit 規則的 NOT_APPLICABLE |
這四個案例不是完整矩陣。
它們只是先建立情境測試包的最小骨架。
Day 15 新增:
src/test/java/com/twlab/qualitygate/validation/ContractScenarioCaseTests.java
這個測試類別不取代 Day 14 的 ContractComparisonServiceTests。
Day 14 的測試重點是:
比較服務本身能不能同時跑 v1.0 / v1.1?
Day 15 的測試重點是:
代表情境的 Expected / Actual 結果是否穩定?
測試用一個小型 expectation table 記錄:
caseName
fixtureName
expected v1.0 Gate
expected v1.1 Gate
expected v1.0 failed rule codes
expected v1.1 failed rule codes
核心測試長這樣:
for (ScenarioExpectation scenario : scenarioExpectations()) {
ContractComparisonResult actual =
comparisonService.compare(fixture(scenario.fixtureName()));
assertThat(actual.v1Result().gateOutcome())
.as("%s v1.0 gate", scenario.caseName())
.isEqualTo(scenario.expectedV1Gate());
assertThat(actual.v1_1Result().gateOutcome())
.as("%s v1.1 gate", scenario.caseName())
.isEqualTo(scenario.expectedV1_1Gate());
assertThat(failedRuleCodes(actual.v1Result()))
.as("%s v1.0 failed rules", scenario.caseName())
.containsExactlyElementsOf(scenario.expectedV1FailedRules());
assertThat(failedRuleCodes(actual.v1_1Result()))
.as("%s v1.1 failed rules", scenario.caseName())
.containsExactlyElementsOf(scenario.expectedV1_1FailedRules());
}
這樣測試失敗時,不只知道 Gate 不符合預期,也可以看到是哪一個情境、哪一個契約版本、哪一組規則結果不一致。
第一個版本差異案例沿用 Day 14 的 fixture:
observation-quantity-wrong-ucum-system.json
這份 Bundle 的 reference 鏈、LOINC code 與可讀 unit 都是乾淨的。
唯一問題是:
"valueQuantity": {
"value": 95,
"unit": "mg/dL",
"system": "http://example.org/local-units",
"code": "mg/dL"
}
所以預期結果是:
| 契約版本 | Gate 結果 | 失敗規則 |
|---|---|---|
| v1.0 | PASSED | 無 |
| v1.1 | BLOCKED | LAB-UNIT-002 |
測試也特別確認:
assertThat(actual.v1Result().contractRuleResults())
.extracting(RuleResult::ruleCode)
.doesNotContain(LabUnit002ObservationUcumCodeRule.RULE_CODE);
也就是 v1.0 不是執行 LAB-UNIT-002 後通過,而是根本沒有啟用這條規則。
如果只有 UCUM 升級案例,讀者可能會誤會:
只要 Gate 被阻擋,就是契約版本升級造成。
所以 Day 15 加入:
missing-internal-reference.json
這份資料的 Observation 指向不存在的 Patient:
Observation.subject.reference = Patient/patient-not-in-bundle
實際結果是:
v1.0 → BLOCKED
v1.1 → BLOCKED
失敗規則固定為:
LAB-REF-001
LAB-REF-003
這個案例的作用是說明:
不是所有版本比較結果都會改變。
有些資料在舊契約與新契約下都不能交換。
原本專案已經有:
observation-value-string.json
它可以用在單條規則測試,因為它只需要證明:
Observation.value[x] 不是 Quantity 時,unit 規則不適用。
但它不適合做整體情境測試,因為它只有 Observation,沒有 Patient 與 DiagnosticReport。
如果直接拿它做 Day 15 情境案例,就會同時觸發 reference 類錯誤。
這會讓測試語意變髒:
想測 NOT_APPLICABLE,卻同時測到 reference fail。
所以 Day 15 新增完整三 Resource Bundle:
non-quantity-observation-bundle.json
Bundle(type=collection)
├─ Patient/patient-1
├─ Observation/obs-value-string-complete
│ ├─ subject → Patient/patient-1
│ ├─ code → LOINC 2345-7
│ └─ valueString → positive
└─ DiagnosticReport/report-1
├─ subject → Patient/patient-1
└─ result → Observation/obs-value-string-complete
這個結構的重點是 reference 鏈保持完整:
Observation.subject 可以找到 Bundle 內的 Patient。
DiagnosticReport.result 可以找到 Bundle 內的 Observation。
DiagnosticReport.subject 與 Observation.subject 指向同一個 Patient。
所以這個案例不會被 Reference 規則擋下來,unit 規則的 NOT_APPLICABLE 結果才是乾淨的。
簡單來說它保留:
唯一不同是 Observation 使用:
"valueString": "positive"
因此 unit 規則可以乾淨地回傳:
LAB-UNIT-001 → NOT_APPLICABLE
LAB-UNIT-002 → NOT_APPLICABLE
這個案例讓第 3 週的測試包不只看到 PASS 與 FAIL,也開始涵蓋四態模型中的 NOT_APPLICABLE。
指令:
./mvnw test
測試結果:
Tests run: 57, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 15 從 Day 14 的 53 個測試增加到 57 個測試。
新增的 4 個測試集中在情境測試包:
LAB-UNIT-002 阻擋。BLOCKED。LAB-UNIT-001 與 LAB-UNIT-002 都回傳 NOT_APPLICABLE。observation-value-string.json 很適合測單條 unit rule。
但它不是完整情境案例。
如果整體 Bundle 同時缺 Patient 或 DiagnosticReport,就很難證明:
這個案例的重點是 NOT_APPLICABLE,不是 reference fail。
所以情境測試包的 fixture 要盡量保持乾淨。
只寫:
assertThat(result.gateOutcome()).isEqualTo(GateOutcome.BLOCKED);
還不夠。因為同樣是 BLOCKED,可能來自:
LAB-REF-001
LAB-CODE-001
LAB-UNIT-002
FHIR R4 validation error
Day 15 的 expectation table 同時記錄 failed rule codes,就是為了避免測試只看結果狀態,卻不知道阻擋原因是否正確。
missing-internal-reference.json 在 v1.0 與 v1.1 都是 BLOCKED。
這提醒我們:
契約版本比較不等於所有錯誤都來自新版本。
只有像 UCUM system/code 這種 v1.1 新增規則造成的結果改變,才適合拿來說明契約升級影響。
ContractScenarioCaseTests。valid-minimal-lab-bundle.json 在 v1.0 / v1.1 都通過。observation-quantity-wrong-ucum-system.json 在 v1.0 通過、v1.1 阻擋。LAB-UNIT-002 阻擋。missing-internal-reference.json 在兩版都阻擋,且失敗規則為 LAB-REF-001、LAB-REF-003。non-quantity-observation-bundle.json。LAB-UNIT-001 與 LAB-UNIT-002 的 NOT_APPLICABLE 情境。./mvnw test 通過,測試數從 53 增加到 57。Day 15 尚未處理:
COMPATIBLE / EXPECTED_BREAKING_CHANGE / UNEXPECTED_REGRESSION 三分類。目前的 MVP 進度:
validation-flow
├─ JSON parse 完成
├─ FHIR R4 parse 完成
├─ FHIR R4 validation 完成
├─ TW Core validation / safe NOT_EVALUATED 完成
├─ Exchange contract rules
│ ├─ LAB-REF-001 完成並接回畫面
│ ├─ LAB-REF-002 完成並接回畫面
│ ├─ LAB-REF-003 完成並接回畫面
│ ├─ LAB-CODE-001 完成並接回畫面
│ ├─ LAB-UNIT-001 完成並接回畫面
│ └─ LAB-UNIT-002 完成並接回畫面
├─ Quality Gate 完成最小版
├─ Contract comparison
│ ├─ ContractVersion 完成最小版
│ ├─ v1.0 / v1.1 rule selection 完成最小版
│ └─ comparison service test 完成最小版
└─ Scenario test pack
├─ minimal expected / actual table 完成最小版
├─ v1.0 / v1.1 representative cases 完成 4 例
└─ NOT_APPLICABLE scenario fixture 完成 1 例
下一步預計處理:
首頁/API 顯示 v1.0 / v1.1 比較
補足更多情境案例的覆蓋矩陣
Repository:twcore-data-quality-gate