iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

如何驗證功能是否正確?

上一章已經把遺留系統的執行路徑、功能規則與例外情況整理成可實作的規格。目標系統完成對應功能後,還需要確認實際結果是否符合這些規格。程式可以順利執行,只能表示沒有在當下中斷,無法說明條件判斷、狀態變化與外部影響是否正確。

本系列採用完整替換的方向。遺留系統與目標系統的內部結構可以不同,驗證重點是兩者在相同條件下是否產生預定的可觀察結果。需要保留的行為應該一致,經過確認的功能調整則應該符合新的驗收條件。所有無法解釋的差異,都要先確認原因才能判定功能已經完成。

先定義哪些行為要一致

開始比較前,要先說明判斷正確與否的依據。遺留系統目前的結果可以協助建立基準,卻不代表每一項結果都符合現在的需求。上一章已經將行為分為預期規則、已知錯誤、歷史相容行為與待確認行為,這些分類要進一步轉換成明確的驗證方式。

行為分類 目標系統的驗證方式
預期規則 目標系統在相同條件下應該產生相同的功能結果。
已知錯誤 目標系統應該符合已確認的修正結果,測試要同時保留遺留系統的原始結果以說明差異。
歷史相容行為 先確認相容對象是否仍在替換範圍內,再按照保留、轉換或移除的決定建立預期結果。
待確認行為 暫時保留為未決問題,不能把目前觀察到的結果直接當成通過條件。

每項允許不同的行為都要記錄需求內容、決定結果、適用範圍與驗收條件。如果只有「這次順便修正」或「新的做法比較合理」等說明,就不足以判定差異已經獲得確認。目標系統出現不同結果時,應該先回到這份紀錄判斷它是預定調整,還是尚未說明的偏差。

比較範圍也要涵蓋失敗結果。成功輸出相同,但拒絕條件、錯誤分類、部分完成狀態或重新執行結果不同,仍可能改變功能行為。定義一致行為時,至少要確認:

  • 相同輸入是否會被接受、拒絕或略過。
  • 相同條件是否會採用一致的判斷規則與預設值。
  • 正常、部分完成與失敗時是否產生預定輸出。
  • 保存狀態是否在相同時機發生預定變化。
  • 檔案、通知、執行紀錄或其他外部影響是否符合規格。
  • 相同內容重複執行時是否產生預定結果。

將功能規格轉換成驗證案例

驗證案例要從已確認的功能規則與輸入輸出範例產生,不能只挑目前容易執行的正常路徑。每項案例都要描述初始條件、操作方式、觀察位置與預期結果,讓遺留系統和目標系統可以使用相同條件執行。

一項可重複使用的案例至少要包含下列內容:

項目 需要記錄的內容
案例識別 可供規格、執行結果與差異紀錄共同引用的唯一名稱。
對應規則 這項案例要驗證的功能規則、需求差異或例外情況。
初始狀態 執行前必須存在或不得存在的保存內容、設定與相依項目狀態。
輸入內容 實際輸入值、格式、順序及必要的觸發方式。
執行步驟 從功能入口到取得結果所需的最少步驟。
觀察項目 需要比較的輸出、狀態變化、錯誤與外部影響。
預期結果 每個觀察項目應該得到的值或符合的條件。
清除方式 執行後如何移除產生的內容,使下一次測試能從相同狀態開始。

案例範圍應該涵蓋主要正常路徑、邊界值、缺少必要內容、格式不符、重複執行、部分完成及相依項目失敗。如果規則會受到多項條件共同影響,應該使用規則表找出會產生不同結果的條件組合,不必列出對結果沒有影響的所有排列。

例如,上一章的匯入功能可能同時處理有效與無效內容。驗證案例不能只檢查畫面顯示的成功數量,還要按照規格確認有效內容的保存結果、無效內容的錯誤資訊、整批處理狀態,以及相同檔案再次執行時的結果。這些觀察項目共同構成功能行為,漏掉其中一項就可能讓目標系統在局部結果正確時被誤判為通過。

建立可以重現的初始條件

相同輸入不一定會產生可比較結果。功能可能受到保存狀態、執行設定、目前時間、自動產生值或相依項目回應影響。如果兩套系統從不同條件開始,執行結果的差異就無法直接歸因於功能實作。

每次比較前,應該確認下列條件:

  1. 兩套系統使用語意相同的輸入資料與初始狀態。
  2. 會影響規則的執行設定已經固定,設定差異也已經記錄。
  3. 相依項目提供相同的回應,或使用可重複控制的測試替代項目。
  4. 目前時間、隨機值與自動產生的識別資料已有明確控制或比較方式。
  5. 前一次執行產生的內容已經清除,或已成為本次案例明確要求的前置狀態。
  6. 兩套系統使用的程式版本、測試資料版本與案例版本都能追溯。

如果兩套系統使用不同的內部資料結構,不需要強迫它們載入完全相同的保存內容。資料準備流程應該把同一個功能情境分別轉換成各系統需要的形式,再確認兩邊表達的欄位意義、關聯與初始狀態一致。

正式資料可能包含不適合進入測試環境的內容。此時應該建立受控樣本,保留會影響功能規則的格式、關聯與邊界情況,並且移除或替換不應保留的內容。自行建立的樣本也要和上一章發現的歷史例外對照,避免只涵蓋理想資料。

依驗證目的選擇測試工具

測試工具可以協助重複執行案例、建立初始條件及顯示結果差異,但通過條件仍要來自已確認的功能規格。應該優先沿用目標系統主要程式語言與建置流程支援的測試框架,再按照相依項目與輸出型態補充其他工具。

使用情境 推薦工具 使用方式與限制
目標程式使用 JavaScript 或 TypeScript Vitest 用來執行單元測試、整合測試、模擬函式及檔案快照。本章範例使用這項工具。
目標程式使用 Python pytest 使用斷言、測試資料準備函式及參數化測試重複執行相同案例。
目標程式使用 Java JUnit 使用測試生命週期、斷言及參數化測試組織功能案例。
功能已確認會透過 HTTP 與相依項目互動 WireMock 固定相依項目的回應內容、錯誤或延遲,讓失敗路徑可以重現。沒有 HTTP 互動時不需要加入。
整合測試需要可實際執行的相依項目,而且測試環境可以啟動容器 Testcontainers 啟動指定版本的暫時性相依項目,載入案例所需的初始狀態,測試後再停止。這種方式不適合取代速度較快的單元測試。

測試框架的語法不同,案例內容仍應該保持一致,包括案例識別、初始狀態、輸入、操作及預期結果。如果遺留系統和目標系統使用不同語言,可以讓兩邊各自使用合適的測試框架,最後輸出共同的比較格式,不需要為了共用工具而改變系統程式。

使用 Vitest 實作匯入案例

以下範例延續前面的匯入功能。importFile 代表目標系統已經完成的功能入口,案例中的函式名稱、輸入格式、狀態名稱與錯誤代碼都要替換成實際規格。第一項案例同時檢查整批狀態、有效內容的保存呼叫與無效內容的拒絕原因。

import { expect, test, vi } from 'vitest'
import { importFile } from './import-file.js'

test('IMP-001:混合內容會保存有效項目並記錄拒絕原因', async () => {
    const save = vi.fn().mockResolvedValue(undefined)
    const result = await importFile(
        {
            fileId: 'file-001',
            rows: [
                { row: 1, code: 'A01', amount: 120 },
                { row: 2, code: '', amount: 80 },
            ],
        },
        {
            wasFileImported: vi.fn().mockResolvedValue(false),
            save,
        },
    )

    expect(result).toEqual({
        status: 'partial',
        imported: 1,
        rejected: [{ row: 2, reason: 'code_required' }],
    })
    expect(save).toHaveBeenCalledTimes(1)
    expect(save).toHaveBeenCalledWith({ code: 'A01', amount: 120 })
})

重複執行案例則要先建立「相同檔案已經處理」的初始狀態,再確認功能沒有再次保存內容。這項案例和上一項案例共用同一個功能入口,但驗證的是不同規則。

test('IMP-003:相同檔案重複執行時不會再次保存內容', async () => {
    const save = vi.fn()
    const result = await importFile(
        {
            fileId: 'file-001',
            rows: [{ row: 1, code: 'A01', amount: 120 }],
        },
        {
            wasFileImported: vi.fn().mockResolvedValue(true),
            save,
        },
    )

    expect(result).toEqual({
        status: 'skipped',
        imported: 0,
        rejected: [],
    })
    expect(save).not.toHaveBeenCalled()
})

模擬函式適合快速驗證目標程式如何使用相依項目。完整路徑的整合測試仍要改用受控的實際保存方式,執行後直接確認保存狀態,避免只驗證函式曾經被呼叫。

使用 WireMock 固定相依項目回應

如果匯入功能已確認會透過 HTTP 傳送處理結果,可以使用 WireMock 建立固定回應。下列對映讓指定路徑穩定回傳 503,用來驗證功能在相依項目失敗時是否保留預定狀態及錯誤資訊。

{
    "request": {
        "method": "POST",
        "urlPath": "/import-results"
    },
    "response": {
        "status": 503,
        "headers": {
            "Content-Type": "application/json"
        },
        "jsonBody": {
            "code": "temporarily_unavailable"
        }
    }
}

測試時要將案例使用的相依項目位置指向 WireMock,並且確認遺留系統與目標系統收到語意相同的回應。實際功能如果沒有 HTTP 互動,應該改用該互動方式可控制的替代項目,不需要為了使用工具而增加 HTTP 介面。

用特徵測試記錄遺留系統行為

特徵測試(Characterization Test)用來記錄系統在固定條件下目前會產生的可觀察結果。這份結果可以建立遺留系統的行為基準,但不會自動判定該行為符合現在的需求。已知錯誤與尚未確認的行為仍要保留分類,不能只因為測試重現成功就改成目標系統的預期結果。

建立特徵測試時,可以按照下列步驟進行:

  1. 從一項已經界定範圍的功能規則選擇代表案例。
  2. 準備固定輸入、初始狀態與可控制的相依項目。
  3. 執行遺留系統,記錄所有需要比較的輸出、狀態變化與外部影響。
  4. 重複執行相同案例,確認結果是否穩定,並找出會變動的欄位。
  5. 將穩定結果保存為基準,標示對應規則、系統版本與取得方式。
  6. 由功能規格判斷哪些基準要原樣保留,哪些項目應該使用已核准的目標結果。

遺留程式碼難以拆開時,可以先從功能入口與可觀察輸出建立整合測試(Integration Test),不必先修改內部程式。這種做法能一次保護完整路徑,適合確認現有行為。缺點是失敗時涵蓋範圍較大,定位問題需要更多資訊。

目標系統可以針對已拆分的功能規則建立單元測試(Unit Test),快速指出哪一項判斷不符合預期。同時仍要保留完整路徑的比較案例,確認各項規則整合後的輸出、狀態變化與外部影響一致。這裡的重點是驗證單一功能的替換結果,整套目標系統的測試層級與執行策略會在後續章節說明。

對相同案例執行雙系統比對

建立遺留系統基準後,可以對目標系統執行相同案例。兩套系統應該在彼此隔離的環境中處理各自的測試內容,避免同時修改同一份狀態,使後執行的一方受到前一方影響。

一次完整比對可以依照下列流程進行:

  1. 使用相同案例版本,分別建立語意一致的初始狀態。
  2. 執行遺留系統並保存原始結果,不要用目標系統的格式覆寫它。
  3. 執行目標系統並保存原始結果,同時記錄使用的程式版本。
  4. 按照事先定義的規則處理可預期的變動欄位,再比較其餘結果。
  5. 逐項比較接受或拒絕結果、輸出內容、狀態變化、錯誤與外部影響。
  6. 將每一項差異連回功能規格,判斷是否符合已核准的需求調整。
  7. 保存比較結果與差異分類,修正後重新執行相同案例。

比較時要保留兩套系統的原始結果與整理後結果。原始結果可以協助確認整理規則是否隱藏重要差異,整理後結果則用來排除已知且沒有功能意義的變動。例如,自動產生的識別資料不需要在兩套系統中具有相同字面值,但兩者都必須符合規定格式,並且在後續輸出中維持正確關聯。

欄位順序、空白或日期格式是否可以忽略,要按照功能規格決定。如果輸出格式是外部項目使用的契約,字面差異就可能具有功能影響。只有已經確認不影響需求的差異,才能加入整理規則。

如果兩套系統都能透過測試轉接函式執行,就可以用參數化測試重複套用相同案例。下列範例只適用於規格要求結果一致的案例。normalizeResult 只能處理已確認沒有功能意義的變動欄位。

import { expect, test } from 'vitest'
import { preservedBehaviorCases } from './import-cases.js'
import { normalizeResult, runLegacyCase, runTargetCase } from './system-runners.js'

test.each(preservedBehaviorCases)('$id:遺留系統與目標系統結果一致', async ({ input }) => {
    const legacyResult = normalizeResult(await runLegacyCase(input))
    const targetResult = normalizeResult(await runTargetCase(input))

    expect(targetResult).toEqual(legacyResult)
})

已核准的功能調整不能套用相同比對斷言。這類案例要另外記錄目標系統的預期結果,並且確認遺留系統的原始結果、需求差異及目標結果都能由同一個案例識別追溯。

用黃金主檔測試比較複雜輸出

輸出內容龐大、結構深或欄位數量多時,逐項撰寫預期值可能容易遺漏。黃金主檔測試(Golden Master Testing)可以先保存一份已確認的完整輸出,後續執行時再將新結果與它比較。這種方式適合結果檔案、結構化內容或包含多個區塊的輸出。

建立黃金主檔時,需要記錄:

  • 使用的案例、輸入內容與初始狀態。
  • 產生輸出的系統版本與執行設定。
  • 已排除或轉換的變動欄位,以及每項處理的原因。
  • 黃金主檔的確認人員、確認時間與適用範圍。
  • 需求變更後更新黃金主檔所需的審查方式。

黃金主檔只會指出結果和既定內容不同,無法自行判定新結果是否更正確。發現差異時,仍要回到功能規格確認原因。直接用最新輸出覆蓋黃金主檔,會使未確認的變更成為新的通過條件,因此每次更新都要保留差異內容與核准紀錄。

如果輸出包含大量無功能意義的變動值,應該先建立結構化的正規化與比較規則。忽略整個區塊雖然可以讓測試穩定,卻可能同時排除真正需要保護的結果。較安全的方式是只處理已知變動欄位,並對欄位格式、關聯或範圍建立獨立檢查。

Vitest 的檔案快照可以把整理後的完整結果保存成黃金主檔。下列範例先獨立檢查每次執行都會變動的識別資料與完成時間,再將它們替換成固定標記,避免直接忽略整個區塊。

import { expect, test } from 'vitest'
import { runTargetCase } from './system-runners.js'

test('IMP-004:複雜匯入結果符合已確認的黃金主檔', async () => {
    const result = await runTargetCase('IMP-004')

    expect(result.runId).toEqual(expect.any(String))
    expect(result.runId.length).toBeGreaterThan(0)
    expect(result.finishedAt).toEqual(expect.any(String))
    expect(Number.isNaN(Date.parse(result.finishedAt))).toBe(false)

    const comparableResult = {
        ...result,
        runId: '<generated-run-id>',
        finishedAt: '<generated-finished-at>',
    }

    await expect(JSON.stringify(comparableResult, null, 2))
        .toMatchFileSnapshot('./golden/IMP-004.json')
})

第一次建立或更新檔案快照後,仍要逐項檢查差異,確認內容符合功能規格再納入版本控制。測試工具提供的更新指令只能產生候選檔案,不能取代黃金主檔的確認流程。

分類並處理不一致結果

雙系統結果不同時,不能直接假設目標系統有錯,也不能因為新結果較符合直覺就接受。每項差異都要先重現,再按照功能規格、原始結果與執行條件判斷原因。

差異分類 判斷方式 處理方式
目標系統缺陷 遺留系統結果符合已確認規則,目標系統結果不符合。 修正目標程式,再重新執行相關案例。
遺留系統已知錯誤 遺留系統重現已確認的錯誤,目標系統符合核准後的結果。 保留差異紀錄,確認修正案例與相關驗收條件都通過。
資料或初始狀態問題 兩套系統的輸入意義、關聯、設定或前置狀態不一致。 修正準備流程,確認條件一致後重新比較。
已核准的功能調整 目標結果與遺留系統不同,但符合已記錄的需求差異。 將決定與驗收結果連回案例,保留為預期差異。
尚未確認的差異 現有規格不足以判斷哪一項結果正確。 記錄影響與確認方式,取得決定前不得標示為通過。

一項差異可能同時出現在多個觀察位置。例如,前面的條件判斷不同,可能連帶造成保存狀態與輸出內容不同。分類時要先找出最早發生的功能差異,再記錄後續影響,避免把同一原因拆成多個互不相關的缺陷。

修正完成後,要重新執行原本失敗的案例,以及使用相同規則或共用元件的相關案例。這樣可以確認修正沒有改變先前已經通過的功能行為。完整的迴歸測試(Regression Testing)範圍會在後續章節納入整體測試策略,本章先確保單一功能的相關案例能重複通過。

保存可追溯的驗證結果

案例、規格、系統版本與執行結果需要建立明確關係。只留下「測試成功」無法說明測了哪些規則、使用甚麼資料,以及當時允許哪些差異。可以使用下列表格追蹤每項功能行為:

案例識別 對應規則 遺留系統結果 目標系統預期 比較結果 差異分類 處理狀態
IMP-001 有效內容應該完成匯入 已完成 已完成 一致 已通過
IMP-002 無效內容應該記錄原因 缺少原因 包含原因 不一致 已核准的功能調整 已通過
IMP-003 重複內容不得產生另一份結果 未產生新內容 產生新內容 不一致 目標系統缺陷 待修正

表格內容只是示意。實際紀錄還要連結案例使用的輸入、初始狀態、完整輸出、程式版本與需求決定。包含自動化測試時,執行結果也要能對應到相同案例識別,避免人工紀錄與測試名稱各自發展。

驗證結果應該隨目標程式一起更新。功能規則改變時,要先更新需求差異與預期結果,再修改測試。測試只因目前程式無法通過而降低檢查範圍,會失去保護功能行為的作用。

判斷功能是否已經通過驗證

功能可以標示為通過前,至少要確認:

  • 所有納入範圍的功能規則都有對應案例,未決規則也已清楚標示。
  • 正常、邊界、錯誤、部分完成與重複執行情況已按照實際風險涵蓋。
  • 測試輸入、初始狀態、執行設定與相依項目可以重複建立。
  • 遺留系統與目標系統的主要輸出、狀態變化及外部影響已經完成比較。
  • 每項不一致結果都有分類、處理決定與重新驗證結果。
  • 所有允許差異都有對應需求、核准紀錄與驗收條件。
  • 案例、原始結果、程式版本與比較結果都可以追溯。

單一代表性功能通過,只能說明相應範圍已經符合替換條件。其餘納入範圍的功能仍要使用相同方式建立案例、執行比較與處理差異。等所有功能、資料與整合條件都通過後,才能進一步判斷目標系統是否具備正式切換條件。

重點整理

  • 功能驗證要先區分必須一致的行為、已核准的功能調整與尚未確認的問題,不能直接把遺留系統的所有結果當成目標需求。
  • 每項驗證案例都要包含對應規則、初始狀態、輸入內容、觀察項目、預期結果與清除方式。
  • 測試框架可以將案例自動化;如果功能確實包含 HTTP 互動或可由容器啟動的相依項目,可以再使用 WireMock 或 Testcontainers 固定測試條件。
  • 特徵測試可以記錄遺留系統目前的行為,目標系統則要同時符合需要保留的基準與已核准的新結果。
  • 雙系統比對要固定輸入、初始狀態、設定與相依項目,並且比較輸出、狀態變化、錯誤及外部影響。
  • 複雜輸出可以使用黃金主檔測試,但每次差異與基準更新仍要按照功能規格確認。
  • 不一致結果要分類為目標系統缺陷、遺留系統已知錯誤、資料或初始狀態問題、已核准的功能調整,或尚未確認的差異。
  • 功能通過驗證前,所有案例、原始結果、程式版本、需求差異與處理決定都要能夠追溯。

上一篇
[Day 14] 如何分析原有功能?
下一篇
[Day 16] 如何拆分大型流程?
系列文
遠古聖遺物改造工程:遺留系統全面重構實務指南19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言