GoStub架構二次開發實踐

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

序言

要寫出好的測試代碼,必須精通相關的測試架構。對於Golang的程式員來說,至少需要掌握下面三個測試架構:

  • GoConvey
  • GoStub
  • GoMock

通過上一篇文章《GoStub架構使用指南》的學習,大家熟悉了GoStub架構的基本使用方法,可以優雅的對全域變數、函數或過程打樁,提高了單元測試水平。

儘管GoStub架構已經解決了很多情境的函數打樁問題,但對於一些複雜的情況,卻只能乾瞪眼:

  1. 被測函數中多次調用了資料庫讀操作函數介面 ReadDb,並且資料庫為key-value型。被測函數先是 ReadDb 了一個父目錄的值,然後在 for 迴圈中讀了若干個子目錄的值。在多個測試案例中都有將ReadDb打樁為在多次調用中呈現不同行為的需求,即父目錄的值不同於子目錄的值,並且子目錄的值也互不相等

  2. 被測函數中多次調用了同一底層操作函數,比如 exec.Command,函數參數既有命令也有命令參數。被測函數先是建立了一個對象,然後查詢對象的狀態,在對象狀態達不到期望時還要刪除對象,其中查詢對象是一個重要的操作,一般會進行多次重試。在多個測試案例中都有將 exec.Command 打樁為多次調用中呈現不同行為的需求,即建立對象、查詢對象狀態和刪除對象對傳回值的期望都不一樣

  3. ...

針對GoStub架構不適用的複雜情況,本文將對該架構進行二次開發,優雅的變不適用為適用,提高GoStub架構的適應能力。

介面

根據開閉原則,我們通過新增介面來應對複雜情況,那麼應該增加兩個介面:

  1. 函數介面
  2. 方法介面

對於複雜情況,都是針對一個函數的多次調用而產生不同的行為,即存在多個傳回值列表。顯然使用者打樁時應該指定一個數組切片[]Output,那麼數組切片的元素Output應該是什麼呢?

每一個函數的傳回值列表的大小不是確定的,且傳回值類型也不統一,所以Output本身也是一個數組切片,Output的元素是interface{}。

於是Output有了下面的定義:

type Output []interface{}

對於函數介面的聲明如下所示:

func StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs

對於方法介面的聲明如下所示:

func (s *Stubs) StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs

但還存在下面兩種情況:

  1. 當被打樁函數在重試調用的情境下,outputs中存在多個相鄰的值是一樣的
  2. 當被打樁函數在多次返回正常且傳回值列表值相同的情境下,outputs中存在多個相鄰的值是一樣的

重複是萬惡之源,我們保持零容忍,所以引入Times變數到Output中,於是Output的定義就演化為:
於是Output有了下面的定義:

type Values []interface{}type Output struct {    StubVals Values    Times int}

介面使用

情境一:多次讀資料庫

假設我們在一個函數f中讀了3次資料庫,比如調用了3次函數ReadLeaf,即通過3個不同的url讀取了3個不同的value。ReadLeaf在db包中定義,樣本如下:

var ReadLeaf = func(url string)(string, error) {    ...}

假設對該函數打樁之前還未產生stubs對象,覆蓋3次讀資料庫的情境的打樁代碼如下:

info1 := "..."info2 := "..."info3 := "..."outputs := []Output{    Output{StubVals: Values{info1, nil}},    Output{StubVals: Values{info2, nil}},    Output{StubVals: Values{info3, nil}},}stubs := StubFuncSeq(db.ReadLeaf, outputs)defer stubs.Reset()...

說明:不指定Times時,Times的值為1

情境二:底層操作有重試

假設我們在一個函數f中調用了3次底層操作函數,比如調用了3次Command函數,即第一次調用建立對象,第二次調用查詢對象的狀態,在狀態達不到期望的情況下第三次掉用刪除對象,其中第二次調用時為了提高正確性,進行了10次嘗試。Command在exec包中定義,屬於庫函數,我們不能直接打樁,所以要在適配層adapter包中進行二次封裝:

var Command = func(cmd string, arg ...string)(string, error) {    ...}

假設對該函數打樁之前已經產生了stubs對象,覆蓋前9次嘗試失敗且第10次嘗試成功的情境的打樁代碼如下:

info1 := "..."info2 := ""info3 := "..."outputs := []Output{    Output{StubVals: Values{info1, nil}},    Output{StubVals: Values{info2, ErrAny}, Times: 9},    Output{StubVals: Values{info3, nil}},}stubs.StubFuncSeq(db.ReadLeaf, outputs)...

介面實現

函數介面實現

函數介面的實現很簡單,直接委託方法介面實現:

func StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs {    return New().StubFuncSeq(funcVarToStub, outputs)}

提供函數介面的目的是,在Stubs對象產生之前就可以使用該介面。

方法介面實現

我們回顧一下方法介面的聲明:

func (s *Stubs) StubFuncSeq(funcVarToStub interface{}, outputs []Output) *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方法的實現,如下所示:

funcPtrType := reflect.TypeOf(funcVarToStub)if funcPtrType.Kind() != reflect.Ptr ||    funcPtrType.Elem().Kind() != reflect.Func {    panic("func variable to stub must be a pointer to a function")}funcType := funcPtrType.Elem()if funcType.NumOut() != len(outputs[0].StubVals) {    panic(fmt.Sprintf("func type has %v return values, but only %v stub values provided", funcType.NumOut(), len(outputs[0].StubVals)))}

構造slice

構造slice的代碼很簡單,如下所示:

slice := make([]Values, 0)for _, output := range outputs {    t := 0    if output.Times <= 1 {        t = 1    } else {        t = output.Times    }    for j := 0; j < t; j++ {        slice = append(slice, output.StubVals)    }}

產生閉包

產生閉包的代碼實現中調用了新封裝的函數getResultValues,如下所示:

i := 0len := len(slice)stubVal := reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value {    if i < len {        i++        return getResultValues(funcPtrType.Elem(), slice[i - 1]...)    }    panic("output seq is less than call seq!")})

新封裝的函數getResultValues的實現參考了StubFunc方法的實現,如下所示:

func getResultValues(funcType reflect.Type, results ...interface{}) []reflect.Value {    var resultValues []reflect.Value    for i, r := range results {        var retValue reflect.Value        if r == nil {            retValue = reflect.Zero(funcType.Out(i))        } else {            tempV := reflect.New(funcType.Out(i))            tempV.Elem().Set(reflect.ValueOf(r))            retValue = tempV.Elem()        }        resultValues = append(resultValues, retValue)    }    return resultValues}

將待打樁函數替換為閉包

這裡直接複用既有的變數打樁方法Stub即可實現,如下所示:

return s.Stub(funcVarToStub, stubVal.Interface())

至此,StubFuncSeq方法實現完了,oh yeah!

反模式

通過上一篇文章《GoStub架構使用指南》的學習,讀者會寫出諸如下面的測試代碼:

func TestFuncDemo(t *testing.T) {    Convey("TestFuncDemo", t, func() {        Convey("for succ", func() {            var liLei = `{"name":"LiLei", "age":"21"}`            stubs := StubFunc(&adapter.Marshal, []byte(liLei), nil)            defer stubs.Reset()            //several So assert        })        Convey("for fail", func() {            stubs := StubFunc(&adapter.Marshal, nil, ERR_ANY)            //several So assert        })    })}

GoStub架構有了StubFuncSeq介面後,有些讀者就會將上面的測試代碼寫成下面的反模式:

func TestFuncDemo(t *testing.T) {    Convey("TestFuncDemo", t, func() {        var liLei = `{"name":"LiLei", "age":"21"}`        outputs := []Output{            Output{StubVals: Values{[]byte(liLei), nil}},            Output{StubVals: Values{ErrAny, nil}},        }        stubs := StubFuncSeq(&adapter.Marshal, outputs)        defer stubs.Reset()        Convey("for succ", func() {            //several So assert        })        Convey("for fail", func() {            //several So assert        })    })}

有的讀者可能認為上面的測試代碼更好,但一般情況下,一個測試函數有多個測試案例,即第二級的Convey數(5個左右很常見)。如果將所有測試案例的樁函數都寫在一起,將非常複雜,而且很多時候會超過人腦的掌握極限,所以筆者將這種模式稱為反模式。

我們提倡每個用例管理自己的樁函數,即分離關注點。

小結

針對GoStub架構不適用的複雜情況,本文對該架構進行了二次開發,包括新增介面StubFuncSeq的定義、使用及實現,優雅的變不適用為適用,提高了GoStub架構的適應能力。本文在最後還提出了StubFuncSeq介面使用的反模式,使得讀者保持警惕,從而正確的使用GoStub架構。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.