自動化管理EP.09

Test Management System (TMS)
從 Legacy Code 到實驗室核心基礎設施

從維護上萬行 legacy code,到逐步推動 Python3 與 React 現代化遷移。
這篇文章分享我如何在不中斷實驗室運作的前提下,重構一套每天被大量使用的測試管理系統。

Joseph Chen

2024 - 2025
14 min read
系統開發

這篇文章要解決什麼問題

在硬體測試與手機量產實驗室中,每天都有成百上千個測試項目在同時進行。在系統化之前,這些測試計畫、執行進度、Bug 追蹤與最終測試報告,全部依賴 Excel 試算表或口頭追蹤。

這會帶來致命的痛點:試算表在多個工程師之間被反覆複製與修改,導致版本極度混亂;測試結果丟失、機台狀態衝突、資訊更新不同步,甚至客戶與工廠的測試規格對不上。

為了解決這些問題,工廠在幾年前上線了一套 **測試管理系統 (Test Management System, 簡稱 TMS)**。然而,隨著業務快速擴張,這套系統也積累了龐大的技術債:舊版 Python 2.7 加上幾萬行揉雜了 SQL、業務邏輯與前端網頁渲染的遺留代碼 (Legacy Code)。

這篇文章要探討的是:當我們面對一套沒有任何單元測試、高耦合度、卻同時支撐著產線數百位工程師每天日常運作的核心 Legacy 系統時,要如何安全、無痛、零停機地進行漸進式重構?


真實案例與 Joseph 的重構經歷

當我剛接手這套 TMS 系統時,心中只有四個字:「如履薄冰」。系統已經運行了四年多,資料庫 Schema 裡充斥著許多沒有外鍵約束的寬表,資料夾中甚至隨處可見 `view_backup_2023.py` 或 `helper_final_fixed.py` 這類的遺留產物。

此時,實驗室的測試經理跑來跟我說:「我們下週要對接客戶最新的自動化測試排程,需要修改資料上傳的 API。但我們不能接受系統停機,因為產線 24 小時都有人在輸入 iPhone 測試數據。」

💡 現場血淚經驗:接手核心系統,最重要不是炫技,而是不讓流程中斷

我一開始也曾想過把整套系統用 FastAPI + Next.js 全面重寫,但很快在現場理智了下來。系統背後連接著無數台自動化測試儀器、條碼掃描槍與客戶端的 API。一旦大改,任何一個接口的微小差異都可能導致整個產線的測試流程瞬間停擺,造成的產值損失是以小時計算的。

後來,我採用了「絞殺者模式 (Strangler Pattern)」:不破壞既有舊接口的運行,但在底層引入新的 Repository 與 Service 抽象層。每當需要修改或新增某個測試階段的業務邏輯時,我們就將舊代碼逐步引導至新架構中。這樣既能逐步淘汰髒代碼,又能保證產線天天順暢運作。


核心概念解釋:遺留系統診斷與重構路徑

在對 TMS 進行手術前,必須先理清資料與任務是如何流轉的。以下是這套現代化 TMS 的核心任務流轉與同步架構:

TMS 測試管理系統工作流程 (Pipeline)

Step 01

分配排程

測試任務排程

Step 02

執行測試

自動化/手動測試

Step 03

結果匯入

Log 提取與解析

Step 04

同步驗證

客戶資料庫同步

Step 05

看板呈現

Dashboard 可視化

重構過程中,我們重點解決了三大核心技術債務:

  • 解耦業務邏輯 (Decoupling):將原本在 Controller 中直接用 Raw SQL 查詢與手動 JSON 拼裝的邏輯,抽離成獨立的 Service 層,便於未來單元測試。
  • 防護性設計與例外處理 (Defensive Coding):對機台自動上傳的非標準格式 Log 進行預先清洗,防止因單一數據異常導致整個排程線程阻塞。
  • 推動運行環境升級 (Python 2 to 3):利用 `six` 和 `2to3` 等工具,逐步將模組遷移至 Python 3.x,解除底層安全漏洞威脅。

程式碼實戰:重構高耦合資料操作

這裡我們用真實遇到的「上傳測試報告」功能為例。第一段是重構前的典型代碼,資料庫連結與格式解析混雜在一起;第二段是引入 Service 與單元測試防護後的結構。

錯誤的寫法:高耦合、缺乏抽象、Raw SQL 拼接與無事務控制(維護極難)

# ❌ 舊 TMS legacy_views.py 中的代碼片段
import pymysql

def upload_test_report(request):
    # 直接在請求處理器中硬編碼處理業務邏輯
    project_id = request.POST.get('project_id')
    tester_name = request.POST.get('tester')
    raw_data = request.POST.get('data') # 格式例如 "test_case_1,PASS;test_case_2,FAIL"

    # 1. 直接連接資料庫,缺乏連線池管理
    conn = pymysql.connect(host='10.120.x.x', user='admin', password='password', db='tms_db')
    cursor = conn.cursor()

    # 2. 拼接 SQL,存在 SQL 注入風險,且無任何錯誤處理
    # 如果 raw_data 解析到一半失敗,前面寫入的資料會卡在髒狀態
    try:
        sql = f"INSERT INTO test_records (project_id, tester) VALUES ('{project_id}', '{tester_name}')"
        cursor.execute(sql)
        record_id = cursor.lastrowid
        
        # 3. 複雜的字串解析與高耦合插入
        for item in raw_data.split(';'):
            case_name, result = item.split(',')
            # 如果 result 格式不正確,會在這裡崩潰,但前面的 record_id 已經寫入
            cursor.execute(f"INSERT INTO case_results (record_id, name, status) VALUES ({record_id}, '{case_name}', '{result}')")
        
        conn.commit()
        return {"status": "success"}
    except Exception as e:
        # 錯誤拋出不完整,且沒有對 conn 進行 rollback,可能造成 DB Lock
        return {"status": "error", "message": str(e)}
    finally:
        cursor.close()
        conn.close()

改良後寫法:Repository-Service 模式,加入事務保護與強型別驗證

# 🚀 重構後的 modern_services.py
from django.db import transaction
from typing import Dict, List, Tuple

class TestRecordRepository:
    """負責資料庫存取的持久層 (Decoupled Data Layer)"""
    @staticmethod
    def create_record(project_id: int, tester: str) -> int:
        # 使用 Django ORM 或參數化 SQL,由底層連線池代管
        record = TestRecord.objects.create(project_id=project_id, tester=tester)
        return record.id

    @staticmethod
    def bulk_insert_results(record_id: int, results: List[Tuple[str, str]]):
        # 批量寫入以優化效能
        objs = [CaseResult(record_id=record_id, name=name, status=status) for name, status in results]
        CaseResult.objects.bulk_create(objs)

class TestReportService:
    """業務邏輯層 (Business Logic Layer)"""
    def __init__(self, repo=TestRecordRepository()):
        self.repo = repo

    def parse_raw_data(self, raw_data: str) -> List[Tuple[str, str]]:
        """將原始資料解析為結構化 Tuple,包含格式驗證 (Validate first)"""
        parsed = []
        if not raw_data:
            raise ValueError("數據不可為空")
            
        for index, item in enumerate(raw_data.split(';')):
            parts = item.split(',')
            if len(parts) != 2:
                raise ValueError(f"第 {index+1} 筆數據格式錯誤: '{item}'")
            name, result = parts[0].strip(), parts[1].strip()
            if result not in ['PASS', 'FAIL', 'BLOCK']:
                raise ValueError(f"無效的測試結果狀態: '{result}'")
            parsed.append((name, result))
        return parsed

    def process_report_upload(self, project_id: int, tester: str, raw_data: str) -> Dict[str, str]:
        # 使用 transaction.atomic 確保 ACID 事務完整性
        try:
            results_to_insert = self.parse_raw_data(raw_data)
            
            with transaction.atomic():
                record_id = self.repo.create_record(project_id, tester)
                self.repo.bulk_insert_results(record_id, results_to_insert)
                
            return {"status": "success", "record_id": str(record_id)}
        except ValueError as ve:
            # 捕獲型別或格式錯誤,返回清晰說明,不拋出 stack trace 洩密
            return {"status": "error", "message": f"驗證失敗: {str(ve)}"}
        except Exception as e:
            # 記錄 log 供監控系統使用
            logger.error(f"上傳測試報告時發生未知系統錯誤: {e}", exc_info=True)
            return {"status": "error", "message": "系統內部錯誤,請聯繫管理員"}

重構策略比較

當面對運作中的核心 Legacy System 時,選擇何種更新策略是考驗架構師技術與情商的關鍵:

重構方案優點缺點適合場景
全面推翻重寫 (Big Bang Rewrite)無歷史包袱、技術棧最新、性能上限高交付週期極長、極易丟失隱藏業務邏輯、產線停擺風險極大舊系統邏輯簡單、或原系統代碼已完全無法編譯運行
漸進式重構 (Incremental Strangler)風險極低、隨時可部分上線交付、產線運作不受干擾新舊代碼會共存一段時間、開發人員需同時維護兩套邏輯核心業務系統、產線不可中斷、邏輯複雜且文件缺失的系統
僅打補丁 (Maintain Only)短期開發成本最低、不需要進行系統設計變更技術債像雪球越滾越大、隨著版本更迭維護難度指數級上升預計在一年內會被徹底淘汰或邊緣化的輔助系統

重構過程中的常見錯誤與踩坑

🔴

直接刪除「看似無用」的反射或動態代碼

在一次程式碼清理中,我看到幾個 Python 檔案內有定義了十幾個沒被任何地方呼叫的 helper 函數,於是順手把他們刪除了。沒想到隔天一早,某個老舊測試機台在上傳報告時報錯。排查後才發現,舊系統竟然是用字串反射方式 `globals()[dynamic_func_name]()` 來動態呼叫那些函數。
正解: 對沒有靜態依賴關係的代碼,刪除前必須在全局進行文字搜尋,並配合日誌檢索,確保無動態反射呼叫後才可移除。

🔴

直接變更核心資料表欄位結構 (Database Schema)

為了優化性能,我將測試紀錄表 `test_records` 中的一個欄位從字串型別改為整數型別。本以為修改了對應的 Django Model 就大功告成,但部署後當場導致其他自動化腳本寫入報錯,因為那些腳本仍繞過 ORM 直接用 raw DB connection 寫入字串。
正解: 資料庫重構應遵循「Add, Migrate, Deprecate」三步法:先新增新欄位,同步寫入雙邊欄位,將歷史數據遷移完畢,修改所有讀取端後,最後才刪除舊欄位。


實務重構建議

實施雙寫與影子測試 (Shadow Testing)

在將關鍵模組(如報表統計邏輯)切換到新架構前,可以在後台進行「雙寫」:同時呼叫新舊兩套代碼,並比對兩者結果是否完全一致,若有差異則發出告警,以此驗證新代碼的準確性。

建立全面邊界測試案例

在開始重構任何一個函數前,先為它補上最基本輸入輸出的單元測試。測試不必覆蓋 100% 邏輯,但一定要覆蓋最常見的邊界條件,這是重構時最強大的安全網。


與本系列其他文章的關聯

這篇 TMS 系統的現代化實戰,與我們在自動化與 AI 部署的其他技術密不可分:


總結

重構並不是要把所有舊代碼全部重寫一遍,而是以最小的成本,讓系統具備持續演進的能力。

在 TMS 重構實戰中,我們透過逐步引導、解耦 Repository-Service 層以及嚴格的格式驗證,成功在零停機的前提下,將這個曾經脆弱的 Legacy 系統轉變為支撐產線高強度測試的核心引擎。這說明了在複雜商業現場,穩健、務實的工程方法永遠重於盲目的技術追新。


延伸閱讀:更多 AI 技術分享