National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 16

測試程式的架構

儀器類別、設定檔、記錄與可替換的儀器驅動
Interaction實作 30 minute(s)

Learning Objectives

  • 把一台儀器的 SCPI 指令包成類別,讓測試程式讀起來像測試步驟
  • 用 __enter__/__exit__ 讓 with 區塊結束時一定關閉輸出
  • 把資源字串與測試條件放進設定檔,換儀器、換條件不必改程式
  • 讓所有通訊經過同一個地方並記錄下來,用介面相同的假儀器在沒有硬體時測試程式

程式長大之後

到目前為止的程式都是一條直線:開儀器、下指令、印結果。程式只有二十行時沒問題; 變成兩百行、有三台儀器、好幾個人一起改的時候,會出現這些狀況:

  • 同一句 psu.write("APPL 5,0.03") 散在十個地方,換一台指令不同的電源供應器要改十個地方。
  • 資源字串寫死在程式裡,實驗室把電錶換到另一個 GPIB 位址,就得改程式。
  • 程式出錯時,不知道最後送了哪一個指令。
  • 沒有接儀器就完全不能測試程式。

這一課介紹測試程式常見的四個整理方式:

問題 做法
指令散落各處 儀器類別(驅動):一台儀器一個類別,SCPI 只出現在類別裡
出錯時輸出沒關 __enter__/__exit__:用 with 保證收尾
條件寫死在程式裡 設定檔:資源字串、電壓、限值放在 JSON 檔
不知道發生什麼事、沒有硬體不能測 記錄所有通訊;介面相同的假儀器可以替換

逐步看:把電源供應器包成類別

PowerSupply 類別就是這台電源供應器的驅動(driver):SCPI 指令只出現在類別裡, 下面的測試程式只呼叫 set()、output()、current()。看執行時怎麼跳進類別的方法裡,又回到主程式。

分層:驅動、測試步驟、主程式

把程式分成幾層,每一層只做一件事:

主程式/測試流程   讀設定檔、決定量哪些點、判斷合格、寫報表
      ↓ 呼叫
儀器驅動(類別)   PowerSupply.set()、DMM.voltage():把動作翻譯成 SCPI
      ↓ 呼叫
PyVISA             送出與接收字串
      ↓
介面與儀器         USB、GPIB、LAN

好處是改動只影響一層:換一台指令不同的電源供應器,只改驅動;改測試條件,只改設定檔;主程式不必動。 許多儀器廠商也提供自己的驅動程式(例如 IVI 驅動,或各廠的 Python 套件),觀念相同:把 SCPI 藏在方法後面。

下一個範例加上兩件事:設定檔,以及讓 with 保證收尾的 __enter__/__exit__。

逐步看:設定檔與 with

station.json 放資源字串、掃描電壓與限流。看最後一步:離開 with 區塊時,程式自動跳進 __exit__,送出 OUTP OFF。 就算 with 裡面出錯,__exit__ 也一樣會執行,效果和上一課的 try/finally 相同,但寫在類別裡,每次用都不會忘。

逐步看:所有通訊都經過同一個地方

Instrument 是所有儀器共用的基底類別,write() 與 query() 都會先記錄再送出。 因為每一筆通訊都經過這兩個方法,只要在這裡加一行,整個程式的通訊都有紀錄;出錯時打開記錄檔,就知道最後送了什麼。

記錄的等級與 logging 模組

剛才的記錄器用了三種等級,實務上常用:

等級 記什麼
DEBUG 每一筆通訊、中間值:除錯時才看
INFO 測試開始、結束、每個步驟的結果
WARN(WARNING) 不正常但還能繼續:例如進入定電流、讀值接近限值
ERROR 測試無法繼續的錯誤

Python 標準函式庫有 logging 模組,可以設定等級門檻、同時輸出到螢幕與檔案、自動加上時間; 在自己的電腦上寫測試程式,建議直接用它(logging.basicConfig(...)、logging.getLogger(...))。 本頁的執行環境不支援 logging,所以範例自己寫了一個最簡單的版本,觀念相同。

記錄要加上時間:真實的記錄器會在每一行前面加日期時間,事後才能和其他事件對照。本頁的範例省略了時間。

逐步看:同一個測試,換一個驅動

LabPSU(真的儀器)和 FakePSU(用公式算的假儀器)有相同的方法名稱:set_volts()、current()、off()。 led_test() 不在乎拿到的是哪一個,只要有這三個方法就能跑。看兩次呼叫 led_test() 時,跳進的是不同類別的方法。

為什麼要能替換驅動

「方法名稱一樣就能互換」在 Python 叫鴨子型別(duck typing):會呱呱叫、會走路,就當它是鴨子。 在測試程式裡,它帶來幾個實際的好處:

  • 沒有硬體也能開發:儀器借不到、還在採購,先用假儀器把流程、報表、錯誤處理寫好。
  • 自動測試測試程式:假儀器可以故意回傳超出限值的讀值、或丟出逾時錯誤,檢查判斷邏輯對不對。
  • 換廠牌只換驅動:兩台不同廠牌的電源供應器各寫一個類別,方法名稱一樣,測試流程不必改。

常見的寫法是在設定檔加一個欄位決定用哪一個驅動:

DRIVERS = {"lab": LabPSU, "fake": FakePSU}
psu = DRIVERS[cfg["driver"]](...)

要讓「方法一樣」更有保障,可以用 abc 模組定義抽象基底類別(ABC),列出每個驅動都必須有的方法。 本頁其實就是一個大型的假儀器:你的程式送出的 SCPI,是由模擬的儀器回答的。

小測驗:類別與 with

1.

下面的程式會印出什麼?

log = []

class Output:
    def __enter__(self):
        log.append("ON")
        return self
    def __exit__(self, *exc):
        log.append("OFF")

try:
    with Output():
        log.append("量測")
        raise TimeoutError
except TimeoutError:
    log.append("逾時")
print(log)
2.

實驗室把數位電錶從 GPIB 位址 22 改成 24。程式哪一種寫法最好改?

3.

下面的程式會印出什麼?

import json
cfg = json.loads('{"volts": [3, 5], "limit_A": 0.03}')
print(len(cfg["volts"]), cfg["limit_A"] * 1000)
第 1 次作答

互動練習:整理成有結構的測試程式

起始程式已經有設定字典 CONFIG 與 PowerSupply 類別,但它有一個問題:CONFIG 裡的 40 V 超過電源供應器的範圍, check() 發現錯誤後丟出例外,輸出就一直開著。依序完成任務:

  1. 直接執行,看程式出錯後電源供應器的面板:輸出還是開的。
  2. 幫 PowerSupply 加上 __enter__/__exit__,改用 with PowerSupply(...) as psu:;先不要改 CONFIG,確認出錯時輸出也會關閉。
  3. 改 CONFIG:電壓改成 2.5、3.5、4.5 V,限流改成 20 mA,程式正常跑完。
  4. 幫類別加一個 identify() 方法,用 *IDN? 取出型號並印出「型號 …」。
  5. 回答一個問題。

小測驗:替換驅動

1.

下面的程式會印出什麼?

class FakeDMM:
    def __init__(self, readings):
        self.readings = list(readings)
    def voltage(self):
        return self.readings.pop(0)

def average(dmm, n):
    return sum(dmm.voltage() for _ in range(n)) / n

d = FakeDMM([1.0, 2.0, 6.0, 9.0])
print(average(d, 3), len(d.readings))
2.

假儀器(模擬驅動)最適合用來做什麼?

第 1 次作答

動手寫:一台假的電錶

寫一個 FakeDMM 類別:建立時給一串讀值;每呼叫一次 voltage() 就依序回傳下一個; 讀值用完了就丟出 RuntimeError("沒有讀值了")。下面的 average_voltage() 不用改,它不知道拿到的是假電錶。

你的輸出
預期輸出

重點整理

  • 一台儀器一個類別(驅動):SCPI 只出現在類別裡,測試程式只呼叫有名字的方法。
  • __enter__/__exit__ 讓 with 區塊結束時一定收尾(關輸出、關連線),出錯也一樣。
  • 資源字串、測試條件放在設定檔(例如 JSON),換儀器、換條件不改程式。
  • 所有通訊經過同一個基底類別,在那裡記錄;記錄分等級,真實的程式用 logging 模組並加上時間。
  • 方法名稱相同的驅動可以互換:假儀器讓你沒有硬體也能開發與測試程式。

下一課用這些積木做產線測試:每個產品一個序號、每個量測都有限值、判斷 Pass/Fail、產生報表。

這一課完成了!
Next lesson:產線測試
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。