iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

摘要
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-001LAB-REF-003
非 Quantity Observation non-quantity-observation-bundle.json PASSED PASSED 覆蓋 unit 規則的 NOT_APPLICABLE

這四個案例不是完整矩陣。
它們只是先建立情境測試包的最小骨架。

ContractScenarioCaseTests 做什麼?

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 不符合預期,也可以看到是哪一個情境、哪一個契約版本、哪一組規則結果不一致。

v1.1 UCUM 升級案例

第一個版本差異案例沿用 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 後通過,而是根本沒有啟用這條規則。

兩版都阻擋的 reference 案例

如果只有 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

這個案例的作用是說明:

不是所有版本比較結果都會改變。
有些資料在舊契約與新契約下都不能交換。

為什麼新增 non-quantity fixture?

原本專案已經有:

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 結果才是乾淨的。

簡單來說它保留:

  • Patient
  • Observation
  • DiagnosticReport
  • 正確 subject / result reference
  • 合約允許的 LOINC code

唯一不同是 Observation 使用:

"valueString": "positive"

因此 unit 規則可以乾淨地回傳:

LAB-UNIT-001 → NOT_APPLICABLE
LAB-UNIT-002 → NOT_APPLICABLE

這個案例讓第 3 週的測試包不只看到 PASSFAIL,也開始涵蓋四態模型中的 NOT_APPLICABLE

自動化驗證

指令:

./mvnw test

測試結果:

Tests run: 57, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

https://ithelp.ithome.com.tw/upload/images/20260816/20177913xhOAAHLIq2.png

Day 15 從 Day 14 的 53 個測試增加到 57 個測試。
新增的 4 個測試集中在情境測試包:

  • 四個代表案例的 Expected / Actual Gate 驗證。
  • v1.1 UCUM 升級案例只因 LAB-UNIT-002 阻擋。
  • reference 錯誤在 v1.0 / v1.1 都維持 BLOCKED
  • 非 Quantity Observation 在 LAB-UNIT-001LAB-UNIT-002 都回傳 NOT_APPLICABLE

常見錯誤 & 排查

  1. 把規則單元測試 fixture 直接拿來做情境測試

observation-value-string.json 很適合測單條 unit rule。
但它不是完整情境案例。

如果整體 Bundle 同時缺 Patient 或 DiagnosticReport,就很難證明:

這個案例的重點是 NOT_APPLICABLE,不是 reference fail。

所以情境測試包的 fixture 要盡量保持乾淨。

  1. 只檢查 Gate,不檢查規則編號

只寫:

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,就是為了避免測試只看結果狀態,卻不知道阻擋原因是否正確。

  1. 把所有差異都解讀成版本升級

missing-internal-reference.json 在 v1.0 與 v1.1 都是 BLOCKED

這提醒我們:

契約版本比較不等於所有錯誤都來自新版本。

只有像 UCUM system/code 這種 v1.1 新增規則造成的結果改變,才適合拿來說明契約升級影響。

今天完成了什麼

  • 新增 ContractScenarioCaseTests
  • 建立四個代表案例的 Expected / Actual Gate 驗證。
  • 驗證 valid-minimal-lab-bundle.json 在 v1.0 / v1.1 都通過。
  • 驗證 observation-quantity-wrong-ucum-system.json 在 v1.0 通過、v1.1 阻擋。
  • 驗證 v1.1 UCUM 升級案例只因 LAB-UNIT-002 阻擋。
  • 驗證 missing-internal-reference.json 在兩版都阻擋,且失敗規則為 LAB-REF-001LAB-REF-003
  • 新增 non-quantity-observation-bundle.json
  • 覆蓋 LAB-UNIT-001LAB-UNIT-002NOT_APPLICABLE 情境。
  • ./mvnw test 通過,測試數從 53 增加到 57。

Day 15 尚未處理:

  • 契約 YAML/JSON 載入。
  • 契約啟用/停用規則的外部設定。
  • Change Manifest。
  • COMPATIBLE / EXPECTED_BREAKING_CHANGE / UNEXPECTED_REGRESSION 三分類。
  • 首頁顯示 v1.0 / v1.1 比較。
  • Docker Compose。
  • GitHub Actions。

目前的 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


上一篇
Day14 - 契約 v1.0 / v1.1 基本比較骨架
下一篇
Day16 - 契約版本比較接回畫面
系列文
醫療資料通過標準驗證,就真的能交換嗎?——30 天打造 TW Core 資料品質閘門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言