GoStub框架二次開發實踐
新媒體管家
每天讀一篇一線開發者原創好文
▍序言
要寫出好的測試代碼,必須精通相關的測試框架。對於Golang的程序員來說,至少需要掌握下面四個測試框架:
GoConvey
GoStub
GoMock
Monkey
儘管GoStub框架已經解決了很多場景的函數打樁問題,但對於一些複雜的情況,卻只能幹瞪眼:
1.被測函數中多次調用了資料庫讀操作函數介面 ReadDb,並且資料庫為key-value型。被測函數先是 ReadDb 了一個父目錄的值,然後在 for 循環中讀了若干個子目錄的值。在多個測試用例中都有將ReadDb打樁為在多次調用中呈現不同行為的需求,即父目錄的值不同於子目錄的值,並且子目錄的值也互不相等
2.被測函數中有一個循環,用於一個批量操作,當某一次操作失敗,則返回失敗,並進行錯誤處理。假設該操作為Apply,則在異常的測試用例中有將Apply打樁為在多次調用中呈現不同行為的需求,即Apply的前幾次調用返回成功但最後一次調用卻返回失敗
3.被測函數中多次調用了同一底層操作函數,比如 exec.Command,函數參數既有命令也有命令參數。被測函數先是創建了一個對象,然後查詢對象的狀態,在對象狀態達不到期望時還要刪除對象,其中查詢對象是一個重要的操作,一般會進行多次重試。在多個測試用例中都有將 exec.Command 打樁為多次調用中呈現不同行為的需求,即創建對象、查詢對象狀態和刪除對象對返回值的期望都不一樣
4....
針對GoStub框架不適用的複雜情況,本文將對該框架進行二次開發,優雅的變不適用為適用,提高GoStub框架的適應能力。
▍介面
根據開閉原則,我們通過新增介面來應對複雜情況,那麼應該增加兩個介面:
1.函數介面
2.方法介面
對於複雜情況,都是針對一個函數的多次調用而產生不同的行為,即存在多個返回值列表。顯然用戶打樁時應該指定一個數組切片[]Output,那麼數組切片的元素Output應該是什麼呢?
每一個函數的返回值列表的大小不是確定的,且返回值類型也不統一,所以Output本身也是一個數組切片,Output的元素是interface{}。
於是Output有了下面的定義:
對於函數介面的聲明如下所示:
對於方法介面的聲明如下所示:
但還存在下面兩種情況:
1.當被打樁函數在批量操作的場景下,即前面幾次都返回成功而最後一次卻返回失敗,outputs中存在多個相鄰的值是一樣的
2.當被打樁函數在重試調用的場景下,即被打樁函數在前面幾次都返回失敗而最後一次卻返回成功,outputs中存在多個相鄰的值是一樣的
重複是萬惡之源,我們保持零容忍,所以引入Times變數到Output中,於是Output的定義就演進為:
於是Output有了下面的定義:
▍介面使用場景一:多次讀資料庫假設我們在一個函數f中讀了3次資料庫,比如調用了3次函數ReadLeaf,即通過3個不同的url讀取了3個不同的value。ReadLeaf在db包中定義,示例如下:
假設對該函數打樁之前還未生成stubs對象,覆蓋3次讀資料庫的場景的打樁代碼如下:
說明:不指定Times時,Times的值為1
▍場景二:批量操作
假設我們在一個函數f中進行批量操作,比如在一個循環中調用了5次Apply函數,前4次操作都成功但第5次操作卻失敗了。Apply在resource包中定義,示例如下:
假設對該函數打樁之前已經生成了stubs對象,覆蓋前4次Apply都成功但第5次Apply卻失敗的場景的打樁代碼如下:
假設對該函數打樁之前已經生成了stubs對象,覆蓋前4次Apply都成功但第5次Apply卻失敗的場景的打樁代碼如下:
▍場景三:底層操作有重試
假設我們在一個函數f中調用了3次底層操作函數,比如調用了3次Command函數,即第一次調用創建對象,第二次調用查詢對象的狀態,在狀態達不到期望的情況下第三次掉用刪除對象,其中第二次調用時為了提高正確性,進行了10次嘗試。Command在exec包中定義,屬於庫函數,我們不能直接打樁,所以要在適配層adapter包中進行二次封裝:
假設對該函數打樁之前已經生成了stubs對象,覆蓋前9次嘗試失敗且第10次嘗試成功的場景的打樁代碼如下:
▍介面實現
函數介面實現
函數介面的實現很簡單,直接委託方法介面實現:提供函數介面的目的是,在Stubs對象生成之前就可以使用該介面。
▍方法介面實現
我們回顧一下方法介面的聲明:
方法介面的實現相對比較複雜,需要藉助反射和閉包這兩個強大的功能。
為了便於實現,我們分而治之,先進行to do list的拆分:
1.入參校驗。(1)funcVarToStub必須為指向函數的指針變數;(2)函數返回值列表的大小必須和Output.StubVals切片的長度相等
2.將outputs中的Times變數都消除,轉化成一個純的多組返回值列表,即切片[]Values,設切片變數為slice
3.構造一個閉包函數,自由變數為i,i的值為[0, len(slice) - 1],閉包函數的返回值列表為slice[i]
4.將待打樁函數替換為閉包函數
▍入參校驗
入參校驗的代碼參考了StubFunc方法的實現,如下所示:
▍構造slice構造slice的代碼很簡單,如下所示:
說明:當Times的值小於等於1時,就按1次記錄,否則按實際次數記錄。這是一個特殊處理,目的是用戶在構造Output時,一般不需要顯式的給Times賦值,除非有多次,這樣就提高了GoStub框架的易用性。
生成閉包
生成閉包的代碼實現中調用了新封裝的函數getResultValues,如下所示:
新封裝的函數getResultValues的實現參考了StubFunc方法的實現,如下所示:
說明:StubFuncSeq要求len(slice)必須大於等於樁函數的調用次數,否則會顯式panic,並有異常日誌"output seq is less than call seq!"。
▍將待打樁函數替換為閉包
這裡直接復用既有的變數打樁方法Stub即可實現,如下所示:
至此,StubFuncSeq方法實現完了,oh yeah!
▍反模式
多個測試用例的樁函數綁定在一起
通過上一篇文章《GoStub框架使用指南》的學習,讀者會寫出諸如下面的測試代碼:GoStub框架有了StubFuncSeq介面後,有些讀者就會將上面的測試代碼寫成下面的反模式:
有的讀者可能認為上面的測試代碼更好,但一般情況下,一個測試函數有多個測試用例,即第二級的Convey數(5個左右很常見)。如果將所有測試用例的樁函數都寫在一起,將非常複雜,而且很多時候會超過人腦的掌握極限,所以筆者將這種模式稱為反模式。
我們提倡每個用例管理自己的樁函數,即分離關注點。
函數返回值列表都相同仍使用StubFuncSeq介面打樁
顯然,StubFuncSeq介面的功能強於StubFunc介面,這就導致有些讀者習慣了使用StubFuncSeq介面,而忽略或很少使用StubFunc介面。
假設函數f中有一個循環,可以從數組切片中獲取到不同用戶的Id,然後根據Id清理該用戶的資源。比如總共有3個用戶,依次調用resource包中的Clear函數進行資源清理,該函數的示例如下:
假設對該函數打樁之前已經生成了stubs對象,覆蓋3次都清理成功的場景的打樁代碼如下:
這段代碼儘管沒毛病,但如果函數通過StubFunc介面打樁,則不管樁函數被調用多少次,都會返回唯一的值列表。
我們重構一下代碼:
很明顯,重構後的代碼簡單了很多。
可見,當函數返回值列表都相同時仍使用StubFuncSeq介面打樁是一種反模式。我們在給函數打樁時,優先使用StubFunc介面,當且僅當StubFunc介面不滿足測試需求時才考慮使用StubFuncSeq介面。
▍小結
針對GoStub框架不適用的複雜情況,本文對該框架進行了二次開發,包括新增介面StubFuncSeq的定義、使用及實現,優雅的變不適用為適用,提高了GoStub框架的適應能力。本文在最後還提出了StubFuncSeq介面使用的兩種反模式,使得讀者時刻保持警惕,從而正確的使用GoStub框架。


TAG:中興開發者社區 |