簡單的 mock 使用情境

今天在寫新功能時, 又陷入小小的苦惱。我要寫一個小函式, 這個函式會依參數從資料庫取出資料, 並依參數決定做那些操作, 再傳回操作完的結果。函式本身需求明確不複雜, 但有不少組合情況要處理好, 有 unit test 比較保險。

在開始 TDD 前, 我得先決定如何準備 fixture (輸入資料)。大概有兩種選擇:
  1. 規劃好目標函式的測試情境, 準備測試用的資料庫, 塞對應的資料進去。
  2. 使用 Mock (我用 pymox)
配合 Django TestCase 和 ORM 操作, 塞入測試資料已簡單許多, 不用自己產生一個同 schema 空的資料庫, 並在每個 test case 前先清空資料, 也不用自己下 SQL 填資料, 所以實作門檻已低了不少。但是仍有幾個問題:
  1. 當測試資料有 relation 時, 要多塞不少相關資料。
  2. 要規劃清楚那些案例需要那些資料。規劃完後, 也需要寫註解畫個表格之類的, 不然很難了解測試資料內容。即使畫了表格, 也不太直覺易懂。
  3. 日後發現新 bug 或改功能時, 不容易修改這堆測試資料。
最讓我受挫的是, 寫完後很難閱讀和維護, 測試案例一多, 根本沒興趣慢慢讀輸入資料的細節。其它像是和外部資源綁在一起、實作目標函式時得先寫相依函式等議題, 在這個例子裡到是沒那麼嚴重。
這回我改試著用 Mock, 將目標函式的介面寫成大概如下的形式:
def target_function(arg1, arg2, getter=db.some_function):
    ...
db 是我另開的模組, 用來放包著 SQL 操作的函式。getter 指向真正用的函式, 測試時則視測試案例傳不同的 mock objects。
使用 Mock 的話, 準備測試資料變得很容易, 而且直接寫明目標函式如何和這些資料互動, 不用看資料庫的資料想像它轉換後的資料。要加測試案例時簡單許多, 不用思考如何在不同表格組出前置資料, 再用這些資料經由資料庫操作轉成中間資料給目標函式用。整體評估下來, 為了測試而多加參數 getter 是很划算的設計, 在註解裡註明一下 getter 的用意即可。

用 Mock 帶來的問題則是:
  1. 視使用的方式, 對目標函式的實作細節需有不同深度的理解。像是 getter 被呼叫幾次, 傳入那些參數。若程式有部份是別人寫的, 需要另外時間讀原本已被隱藏的細節。 
  2. 目標函式改變使用 getter 的方式時, 也需更新準備 mock 的測試程式。
  3. 無法保證目標函式正確, 實際使用目標函式時會有正確成果, 因為 getter 有可能出錯。導致有可能要另測 getter。相對來說, 直接存取資料庫的例子, 不測 getter 的風險較小。
換個角度來看, 測試程式使用 Mock 相當於切開和資料庫的連繫, 卻增加和 getter 操作的連繫。

留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置