目標系統的單一功能通過驗證,只能確認該功能在指定條件下符合預期。完整替換遺留系統前,還要確認各項功能可以正確組合、輸入輸出維持相容、重要操作能完成,以及已確認的品質與風險處理條件都已通過。
前面章節建立的功能案例、既有行為基準與核准差異,會成為本章的測試輸入。本章不再重複單一案例的建立方式,而是把這些案例組成可追溯的整體測試策略,並且定義哪些結果會阻擋正式切換。
測試範圍要從已確認資訊建立,不能只按照現有程式結構或測試工具提供的功能決定。開始安排測試前,應該彙整下列內容:
每項內容都要有明確的預期結果。像是「完成整合測試」只能說明採用了某種測試層級,無法判斷哪些需求已經受到保護。測試範圍應該使用需求或風險識別連接案例,讓失敗結果能回到原本要確認的條件。
單一功能的案例紀錄已經包含初始狀態、輸入、預期結果與清除方式。整體測試策略要進一步記錄案例在系統層級的用途、執行位置及阻擋條件。
| 需求或風險識別 | 案例識別 | 測試目的與層級 | 執行條件 | 正式切換條件 |
|---|---|---|---|---|
REQ-IMP-01 |
E2E-001 |
端對端測試,確認有效匯入流程 | 使用受控輸入檔案與獨立輸出位置 | 必須通過 |
RISK-DUP-01 |
REG-014 |
迴歸測試,確認重複執行結果 | 從相同初始狀態執行兩次 | 必須通過 |
DIFF-IMP-02 |
UAT-001 |
驗收測試,確認核准差異 | 使用已確認的混合內容案例 | 需要指定責任者確認 |
實際紀錄還要包含案例版本、目標程式版本、測試資料版本、執行環境、實際結果與處理狀態。同一個案例識別應該出現在測試名稱與結果紀錄中,避免自動化測試和驗收文件各自使用不同名稱。
為了比較不同測試層級,後續 Vitest 範例使用同一個已確認情境:目標系統會在同一執行環境中讀取匯入檔案、保存有效內容、記錄無效內容,並且避免相同檔案重複產生結果。範例中的函式名稱、輸入格式與預期值都要按照實際規格替換。
測試框架負責執行案例、準備測試條件、比較結果及產生報告。選擇時應該先配合目標系統使用的程式語言、建置方式與執行環境,再比較斷言、測試準備、參數化、模擬及擴充能力。
| 測試框架 | 適用技術 | 主要能力 | 選擇時要確認的內容 |
|---|---|---|---|
| Vitest | JavaScript、TypeScript 與 Vite 生態系 | 提供斷言、模擬、快照、涵蓋率及瀏覽器模式,並且支援 TypeScript 與 JSX | 確認專案轉換設定、執行環境,以及整合或端對端測試所需的實際相依項目 |
| pytest | Python | 提供詳細斷言結果、自動尋找測試、測試準備函式、參數化及外掛擴充 | 確認測試準備函式的範圍、清除方式、外掛版本及非同步程式的支援方式 |
| JUnit | Java 與其他 JVM 語言 | 透過 JUnit Platform 與 JUnit Jupiter 提供測試生命週期、斷言、參數化及擴充模型 | 確認 Java 版本、建置工具整合,以及既有 JUnit 測試的遷移方式 |
| NUnit | .NET 語言 | 提供測試類別、斷言、參數化案例、測試前後處理及多種執行方式 | 確認 .NET 版本、測試執行器、平行執行設定與測試狀態清除方式 |
這四種工具都能支援多種測試目的,但不會自動產生正確的驗收條件。案例仍要來自已確認的需求、風險與既有行為。相依項目模擬、容器化測試環境、瀏覽器操作或效能負載如果需要其他工具,也應該按照實際情境補充,不能只因為測試框架可以執行程式就省略真實互動驗證。
單元測試(Unit Test)驗證可以獨立執行的功能規則、資料轉換或狀態判斷。它應該能快速執行,並且在失敗時指出是哪一項規則不符合預期。時間、隨機值或相依項目如果會影響結果,需要先建立可控制的邊界。
下列 Vitest 範例只驗證數量與單價的計算規則,不載入檔案,也不保存結果:
import { expect, test } from 'vitest'
import { calculateAmount } from './calculate-amount.js'
test('UNIT-001:數量與單價會產生正確總額', () => {
const result = calculateAmount({
quantity: 3,
unitPrice: 120,
})
expect(result).toBe(360)
})
如果這項測試失敗,可以直接檢查計算規則。它不負責確認檔案能否讀取或結果能否保存,這些互動要由其他測試層級處理。
整合測試(Integration Test)驗證組成項目與實際相依項目能否按照規格互動。如果系統包含保存機制、檔案交換、訊息傳遞或外部互動,測試要確認交換內容、失敗處理與狀態變化,不能只檢查模擬函式曾經被呼叫。
下列情境已確認系統會把匯入結果保存成 JSON 檔案。測試使用實際檔案系統,並且在完成後清除暫存內容:
import { expect, test } from 'vitest'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { saveImportResult } from './save-import-result.js'
test('INT-001:匯入結果會寫入指定檔案', async () => {
const directory = await mkdtemp(join(tmpdir(), 'import-result-'))
const filePath = join(directory, 'result.json')
try {
await saveImportResult(filePath, { imported: 2 })
const saved = JSON.parse(await readFile(filePath, 'utf8'))
expect(saved).toEqual({ imported: 2 })
} finally {
await rm(directory, { recursive: true })
}
})
這項測試能發現檔案路徑、編碼、序列化與寫入行為的問題。其他保存方式應該使用相應的實際元件,不需要為了沿用範例而改成檔案。
契約測試(Contract Testing)確認兩個項目對交換內容具有一致理解。契約可能存在於函式公開介面、結構化檔案、訊息或其他已確認的互動方式,不限定為特定網路介面。
契約至少要描述必要欄位、型別、語意、允許值、錯誤格式及版本相容規則。提供內容與讀取內容的兩端都要使用相同契約,否則單獨驗證其中一端仍可能留下不相容差異。
下列 Vitest 範例確認第一版匯入結果具有必要欄位與允許狀態:
import { expect, test } from 'vitest'
import { createImportResult } from './create-import-result.js'
test('CONTRACT-001:匯入結果符合第一版契約', () => {
const result = createImportResult({ imported: 2, rejected: [] })
expect(result).toMatchObject({
schemaVersion: 1,
status: expect.stringMatching(/^(completed|partial|failed)$/),
imported: expect.any(Number),
rejected: expect.any(Array),
})
expect(result.imported).toBeGreaterThanOrEqual(0)
})
toMatchObject 允許結果加入契約沒有禁止的新欄位。如果交換規格要求欄位完全一致,測試就要改用嚴格比較。是否允許擴充應該由版本相容規則決定。
端對端測試(End-to-End Test)從系統實際入口觸發操作,經過主要組成項目後檢查輸出、保存狀態與外部影響。它適合保護少量關鍵流程,不適合承擔所有條件組合,因為完整環境的準備成本較高,失敗時也需要更多資訊才能定位原因。
下列 Vitest 範例從公開匯入入口讀取固定檔案,並且確認執行結果及實際輸出檔案:
import { expect, test } from 'vitest'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runImport } from './run-import.js'
test('E2E-001:有效輸入會完成匯入並保存結果', async () => {
const directory = await mkdtemp(join(tmpdir(), 'import-flow-'))
const outputPath = join(directory, 'result.json')
const inputPath = new URL('./fixtures/valid-records.csv', import.meta.url)
try {
const result = await runImport({ inputPath, outputPath })
const saved = JSON.parse(await readFile(outputPath, 'utf8'))
expect(result.status).toBe('completed')
expect(saved.imported).toBe(2)
} finally {
await rm(directory, { recursive: true })
}
})
這項案例只保護一條重要完整流程。邊界值與拒絕條件仍應該優先放在較快、責任更集中的測試中,再為跨項目失敗或正式切換必要情境補充端對端案例。
迴歸測試(Regression Testing)是一種測試目的,可以包含單元、整合、契約及端對端案例。修正缺陷、調整結構或更新相依項目後,重新執行相關案例,確認先前通過的行為沒有出現非預期變化。
既有系統的所有結果不會自動成為迴歸基準。需要保留的行為沿用既有預期,已核准差異則使用目標結果。尚未確認的差異要保留為未決項目,不能先寫成通過斷言。
下列 Vitest 範例保護曾經修正的重複匯入問題:
import { expect, test } from 'vitest'
import { createImportState, importFile } from './import-file.js'
test('REG-014:相同檔案重複執行不會增加結果', async () => {
const state = createImportState()
const input = {
fileId: 'file-001',
rows: [{ code: 'A01', quantity: 2 }],
}
await importFile(input, state)
await importFile(input, state)
expect(state.records).toEqual([{ code: 'A01', quantity: 2 }])
})
案例識別 REG-014 應該連回原始故障、修正內容與受影響規則。如果相關規則也出現在其他完整流程,還要重新執行對應的整合或端對端案例。
使用者驗收測試(User Acceptance Testing, UAT)確認目標系統能支援已確認的實際操作方式。這項測試需要由需求決策者、操作人員或其他已指定的驗收責任者判斷結果是否符合需求,特別是遺留系統與目標系統預定不同的地方。
可以先將可客觀判斷的部分自動化,讓每次驗收使用相同輸入與預期結果。下列 Vitest 範例執行已確認的混合內容案例:
import { expect, test } from 'vitest'
import { runApprovedImportFlow } from './run-approved-import-flow.js'
test('UAT-001:混合內容會完成有效項目並列出拒絕原因', async () => {
const result = await runApprovedImportFlow({
rows: [
{ row: 1, code: 'A01', quantity: 2 },
{ row: 2, code: '', quantity: 1 },
],
})
expect(result).toEqual({
status: 'partial',
imported: 1,
rejected: [{ row: 2, reason: 'code_required' }],
})
})
自動化結果只能說明程式符合已寫入的條件。驗收責任者仍要確認案例代表實際操作方式、拒絕原因可以支援後續處理,以及這項差異已經獲得核准。驗收紀錄需要保存確認人員、版本、案例與結果。
測試金字塔(Test Pyramid)可以協助思考測試數量與執行成本。通常會以較多快速且範圍集中的測試保護功能規則,再用較少的整合與端對端測試確認實際互動。不過,這是一項配置參考,不是所有系統都必須遵守的固定比例。
測試組合要同時考量下列因素:
程式碼涵蓋率只能協助找出沒有執行到的路徑。高涵蓋率不代表斷言正確,也無法說明重要需求與風險已經受到保護。判斷缺漏時,應該先檢查需求與風險追溯表,再將涵蓋率作為補充資訊。
測試結果要能重現,執行前提就必須可以重新建立。每項測試應該使用已知初始狀態,固定會影響結果的設定、時間與隨機值,完成後清除產生內容。測試資料要保留會影響規則的格式、關聯與邊界情況,同時避免包含不需要的正式敏感內容。
無法穩定控制的相依項目可以先使用測試替身(Test Double),重現正常結果、拒絕條件、逾時或其他已確認的失敗。測試替身只能確認目標程式如何處理預定回應,不能證明實際互動一定相容。因此,正式切換前仍要在受控環境中執行必要的實際整合測試。
測試失敗時,要先判斷原因屬於目標系統缺陷、案例問題、環境問題、相依項目異常或已核准差異。只靠重複執行直到通過,會隱藏結果不穩定或狀態未清除等問題。修正後應該從相同初始條件重新執行,並且保存處理結果。
單元、整合、契約與端對端測試主要描述功能與互動範圍。目標系統如果具有資料遷移、安全保護、效能容量、失敗復原或部署後檢查需求,還要將相應的專項測試結果納入整體通過條件。相關方法由前面或後續章節說明,本章只確認它們能連回需求、風險與正式切換門檻。
Vitest 可以讀取專項工具產生的結構化結果並執行門檻斷言,但不會因此變成效能負載或安全檢查工具。以下範例只確認容量測試報告符合已核准的處理時間與失敗比例:
import { expect, test } from 'vitest'
import { readCapacitySummary } from './read-capacity-summary.js'
test('PERF-GATE-001:容量測試結果符合核准門檻', async () => {
const summary = await readCapacitySummary('./reports/capacity.json')
expect(summary.p95Milliseconds).toBeLessThanOrEqual(800)
expect(summary.failureRate).toBeLessThanOrEqual(0.01)
})
報告仍要由適合的專項工具與受控情境產生,並且記錄工具版本、資料規模、執行條件與原始結果。只把人工填寫的數值交給 Vitest 比較,無法證明專項測試已經正確執行。
目標系統具備正式切換條件前,至少要確認下列結果:
適合自動執行的測試還要記錄執行前提、必要頻率與失敗時的阻擋條件。如何把這些測試放入持續整合(Continuous Integration, CI)、發布與部署流程,會由後續章節進一步說明。