如何在測試中更好地使用mock

注意:本文大部份內容爲翻譯 Bob 大叔的文章,原文連接能夠在文章底部的參考文檔處找到。html

什麼是 mock

mock 做爲名詞時表示 mock 對象,在維基百科的解釋中以下:程序員

在面向對象程序設計中,模擬對象(英語:mock object,也譯做模仿對象)是以可控的方式模擬真實對象行爲的假的對象。程序員一般創造模擬對象來測試其餘對象的行爲。web

mock 做爲動詞時表示編寫使用 mock 對象。數據庫

mock 多用於測試代碼中,對於不容易構造或者不容易獲取的對象,使用一個虛擬的對象來方便測試。編程

mock 的分類

爲了使用示例說明各個mock 種類的區別與聯繫,文章使用 go 語言做爲示例,以下爲示例的基礎代碼:安全

type Authorizer interface {
    authorize(username, password string) bool
}

type System struct {
    authorizer Authorizer
}

func NewSystem(authorizer Authorizer) *System {
    system = new(System)
    system.authorizer = authorizer
    return system
}

func (s *System) loginCount() int {
    // skip
    return 0
}

func (s *System) login(username, password string) error {
    if s.authorizer.authorize(username, password) {
        return nil
    }
    return errors.New("username or password is not right")
}
複製代碼

dummy

當你不關心傳入的參數被如何使用時,你就應該使用 dummy 類型的 mock,通常用於做爲其餘對象的初始化參數。示例以下:服務器

type DummyAuthorizer struct {}
func (d *DummyAuthorizer) authorize(username, password string) bool {
    // return nil
    return false
}

// Test
func TestSystem(t *testing.T) {
    system := NewSystem(new(DummyAuthorizer))
    got := system.loginCount()
    want := 0
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}
複製代碼

在上面的測試示例代碼中,DummyAuthorizer 的做爲只是爲了初始化 System 對象的須要,後續測試中並無使用該 DummyAuthorizer 對象。網絡

注意:此處的 authorize 方法原文返回了 null ,因爲 go 語言不容許爲 bool 返回 nil ,所以此處返回了 false架構

stub

當你只關心方法的返回結果,而且須要特定返回值的時候,這時候你就可使用 stub 類型的 mock 。好比咱們須要測試系統中某些功能是否能正確處理用戶登陸和不登陸的狀況,而登陸功能咱們已經在其餘地方通過測試,並且使用真實的登陸功能調用又比較的麻煩,咱們就能夠直接返回已登陸或者未登陸狀態來進行其餘功能的驗證。函數

type AcceptingAuthorizerStub struct {}

func (aas *AcceptingAuthorizerStub) authorize(username, password string) bool {
    return true
}

type RefusingAuthorizerStub struct {}

func (ras *RefusingAuthorizerStub) authorize(username, password string) bool {
    return false
}
複製代碼

spy

當你不僅是隻關心方法的返回結果,還須要檢查方法是否真正的被調用了,方法的調用次數等,或者須要記錄方法調用過程當中的信息。這個時候你就應該使用 spy 類型的 mock ,調用結束後你須要本身檢查方法是否被調用,檢查調用過程當中記錄的其餘信息。可是請注意,這將會使你的測試代碼和被測試方法相耦合,測試須要知道被測試方法的內部實現細節。使用時須要謹慎一些,不要過渡使用,過渡使用可能致使測試過於脆弱。

type AcceptingAuthorizerSpy struct {
    authorizeWasCalled bool
}

func (aas *AcceptingAuthorizerSpy) authorize(username, password string) bool {
    aas.authorizeWasCalled = true
    return true
}

// Test
func TestSystem(t *testing.T) {
    authorizer := new(AcceptingAuthorizerSpy)
    system := NewSystem(authorizer)
    got := system.login("will", "will")
    if got != nil {
        t.Errorf("login failed with error %v", got)
    }
    
    if authorizer.authorizeWasCalled != true {
        t.Errorf("authorize was not called")
    }
}
複製代碼

mock

mock 類型的 mock 能夠算做是真正的 」mock「 。把 spy 類型的 mock 在測試代碼中的斷言語句移動到 mock 對象中,這使它更關注於測試行爲。這種類型的 mock 對方法的返回值並非那麼的感興趣,它更關心的是哪一個方法被使用了什麼參數在什麼時間被調用了,調用的頻率等。這種類型的 mock 使得編寫 mock 相關的工具更加的簡單,mock 工具能夠幫助你在運行時建立 mock 對象。

type AcceptingAuthorizerVerificationMock struct {
    authorizeWasCalled bool
}

func (aavm *AcceptingAuthorizerVerificationMock) authorize(username, password string) bool {
    aavm.authorizeWasCalled = true
    return true
}

func (aavm *AcceptingAuthorizerVerificationMock) verify() bool {
    return aavm.authorizeWasCalled
}
複製代碼

fake

fake 類型的 mock 與其餘類型的 mock 最大的區別是它包含了真實的業務邏輯。當以不一樣的數據調用時,你會獲得不一樣的結果。隨着業務邏輯的改變,它可能也會愈來愈複雜,最終你也須要爲這種類型的 mock 編寫單元測試,甚至最後它可能成爲了一個真實的業務系統。若是不是必須,請不要使用 fake 類型的 mock 。

type AcceptingAuthorizerFake struct {}

func (aas *AcceptingAuthorizerFake) authorize(username, password string) bool {
    if username == "will" {
    	return true   
    }
    return false
}
複製代碼

總結

mock 是 spy 的一種類型,spy 又是 stub 的一種類型,而 stub 又是 dummy 的一種類型,可是 fake 與其餘全部 mock 類型不一樣,fake 包含了真實的業務邏輯,而其餘類型的 mock 都不包含真實的業務邏輯。

根據 Bob 大叔的實踐來看,他使用最多的是 spy 和 stub 類型的 mock ,而且他不會常用 mock 工具,不多使用 dummy 類型的 mock ,只有在使用 mock 工具時纔會使用 mock 類型的 mock 。如今的編程 IDE 中,只須要你定義好接口,IDE 就能夠幫你輕鬆的實現他們,你只須要簡單的修改就能夠實現 spy 和 stub 類型的 mock ,所以 Bob 大叔不多使用 mock 工具。

mock 的使用時機

mock 對象是一個強大的工具,可是 mock 對象也有兩面性,若是使用不正確也可能會帶來強大的破壞力。

徹底不使用 mock

若是咱們徹底不使用 mock ,直接使用真實的對象進行測試,這會帶來什麼問題呢?

  • 測試將會運行緩慢。咱們使用真實的數據庫,真實的上游服務,因爲這些都須要經過網絡來進行通訊,這會將比程序內部的函數調用慢上幾個數量級。當咱們修改一行簡單的代碼,進行測試時,可能須要等待數分鐘,數小時,甚至可能要幾天才能把測試運行結束。
  • 代碼的測試覆蓋率可能會下降不少。一些錯誤和異常在沒有使用 mock 的狀況下可能根本沒法進行測試,例如網絡協議的異常。一些危險的測試用例,好比刪除文件、刪除數據庫表很難進行安全的測試。
  • 測試變得異常的脆弱。與測試無關的其餘問題可能會致使測試失敗,例如因爲機器負載致使的網絡時延問題,數據庫表的結構不正確,配置文件被錯誤修改等問題。

在徹底不使用 mock 對象的狀況下,咱們的測試會變得緩慢、不完整、脆弱。

過分使用 mock

若是過分使用 mock 對象,全部的測試都使用 mock 對象,這會帶來什麼問題呢?

  • 測試將會運行緩慢。一些 mock 工具強依賴反射機制,所以會使得測試變慢。
  • mock 全部類之間的交互,會致使你必須建立返回其餘 mock 類的 mock 類,你可能須要 mock 整個交互鏈路上全部的類,這將會致使你的測試異常的複雜,而且全部交互鏈路上的 mock 類可能都耦合在了一塊兒,當其中一個修改時,可能會致使整個測試失敗。
  • 暴露本不須要暴露的接口。因爲須要 mock 每個類之間的交互,就須要爲每個類之間的交互建立接口,這將會致使你須要建立出許多隻用於 mock 對象的接口,這是一種過分抽象和可怕的設計損壞。

過分使用 mock 對象,將會使用測試變得緩慢、脆弱、複雜,而且有可能損壞你的軟件設計。

mock 的使用建議

在架構的重要邊界使用 mock ,不要在邊界內部使用 mock

例如能夠在數據庫、web服務器等全部第三方服務的邊界處使用 mock 。能夠參考以下的整潔架構圖:

能夠在最外環的邊界處使用 mock 隔離外部依賴,方便測試,這樣作能夠獲得以下的好處:

  • 測試運行速度快。
  • 測試不會由於外部依賴的錯誤而失敗。
  • 更容易的模擬測試外部依賴的全部異常狀況。
  • 橫跨邊界的有限狀態機的每條路徑均可以被測試。
  • mock 不在須要相互耦合依賴,代碼會更整潔。

另外一個比較大的好處是它強迫你思考找出軟件的重要邊界,而且爲它們定義接口,這使得你的軟件不會強耦合依賴於邊界外的組件。所以你能夠獨立開發部署邊界兩邊的組件。像這樣去分離架構關注點是一個很好的軟件設計原則。

使用你本身的 mock

mock 工具備它們本身的領域語言,在使用它們以前你必須先學習它。經過前面的 mock 類型介紹,咱們已經知道用的最多的 mock 是 stub 和 spy 類型,而因爲如今的 IDE 能夠很方便的生成這些 mock 代碼,咱們只須要稍做修改就能夠直接使用,因此綜合來看,咱們通常狀況下是不須要使用 mock 工具的。

因爲你本身寫 mock 時不會使用反射,這將會讓你的測試代碼運行速度更快。若是你決定使用 mock 工具,請儘可能少的使用它。

總結

mock 對象既不能徹底不使用,也不能過分使用。咱們應該在軟件的重要邊界處使用 mock ,要儘可能少的使用 mock 工具,使用 mock 工具時不要過分依賴它,咱們應該儘可能使用輕量級的 stub 和 spy 的 mock 類型,而且咱們應該本身手寫這些簡單的 mock 類型。若是你這樣作了,你會發現你的測試運行速度更快,更穩定,而且還會有更高的測試覆蓋率,你的軟件架構設計也會愈來愈好。

參考文檔

相關文章
相關標籤/搜索