GOLANG最容易做測試MOCK

winlin發表於2017-06-09

測試時,一些底層的庫非常難以MOCK,比如HASH摘要演算法,怎麼MOCK?假設有個函式,是用MD5做摘要:

func digest(data []byte, h hash.Hash) ([]byte, error) {
    if _, err = h.Write(data); err != nil {
        return nil, errors.Wrap(err, "hash write")
    }

    d := h.Sum(nil)
    if len(d) != 16 {
        return nil, errors.Errorf("digest's %v bytes", len(d))
    }
    return d,nil
}

難以覆蓋的因素有幾個:

  1. 私有函式,一般其他語言在utest中只能訪問public函式,而golang的utest是和目標在同一個package,所有函式和資料都可以訪問。
  2. 有些函式非常難以出錯,但是不代表不出錯,比如這裡的Write方法,一般都是不會有問題的,但是測試如果覆蓋不到,保不齊哪天跑到這一行就掛掉了。
  3. MOCK樁物件或者函式,如果總是要把目標全部實現一遍,比如hash這個介面有5個方法,對Write打樁時只需要覆蓋這個函式,其他的可以不動。是的,聰明的你可能會想到繼承,但是如果這個類是隱藏的呢?比如一個md5的實現是隱藏不能訪問的,暴露的只有hash的介面,怎麼從md5這個類繼承呢?GOLANG提供了類似從實現了介面物件的介面繼承的方式,實際上是組合,具體看下面的實現。
  4. 有些古怪的邏輯,比如這裡判斷摘要是16位元組,一般情況下也不會出現錯誤,當然utest也必須得覆蓋到,萬一哪天用了一個hash演算法跑到這個地方,不能出現問題。

Remark: 注意到這個地方用了一個errors的package,它可以列印出問題出現的堆疊,參考Error最佳實踐.

用GOLANG就可以完美解決上面所有的覆蓋問題,先上程式碼:

type mockMD5Write struct {
    hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
    return 0,fmt.Errorf("mock md5")
}

就這麼簡單?對的,但是不要小看這幾行程式碼,深藏功與名~

組合介面

結構體mockMD5Write裡面巢狀的不是實現md5雜湊的類,而是直接巢狀的hash.Hash介面。這個有什麼厲害的呢?假設用C++,看應該怎麼搞:

class Hash {
public: virtual int Write(const char* data, int size) = 0;
public: virtual int Sum(const char* data, int size, char digest[16]) = 0;
public: virtual int Size() = 0;
};

class MD5 : public Hash {
// 省略了實現的程式碼
}

class mockMD5Write : public Hash {
    private: Hash* imp;
    public: mockMD5Write(Hash* v) {
        imp = v;
    }
    public: int Write(const char* data, int size) {
        return 100; // 總是返回個錯誤。
    }
};

是麼?錯了,mockMD5Write編譯時會報錯,會提示沒有實現其他的介面。應該這麼寫:

class mockMD5Write : public Hash {
    private: Hash* imp;
    public: mockMD5Write(Hash* v) {
        imp = v;
    }
    public: int Write(const char* data, int size) {
        return 100; // 總是返回個錯誤。
    }
    public: int Sum(const char* data, int size, char digest[16]) {
        return imp->Sum(data, size, digest);
    }
    public: int Size() {
        return imp->Size();
    }
};

對比下夠浪的介面組合,因為組合了一個hash.Hash的介面,所以它也就預設實現了,不用再把函式代理一遍了:

type mockMD5Write struct {
    hash.Hash
}
func (v *mockMD5Write) Write(p []byte) (n int, err error) {
    return 0,fmt.Errorf("mock md5")
}

這個可不是少寫了幾行程式碼的區別,這是本質的區別,我雞凍的辯解道~如果這個介面有十個函式,我們要測試100個介面呢?這個MOCK該怎麼寫?另外,這個實際上是OO和GOLANG的細微差異,GOLANG的介面是契約,只要滿足就可以,面向的全是動作,GOLANG像很多函式組合,它沒有類體系的概念,也就是它的結構體不用明顯符合哪個介面和哪個介面它才是合法的,實際上它可以符合任何適配的介面,也就是Die()這個動作,是自動被所有會Die的物件適配了的,不用顯式宣告自己會Die,關注的不是宣告和實現了介面的關係,而是關注動作或者說介面本身,!@#$%^&*()$%^&*(#$%^&*#$^&不能說了,說多了都懂了我還怎麼裝逼去~

複雜錯誤

我們用了errors這個包,用來返回複雜錯誤,可以看到堆疊資訊,對於utest也是一樣,能看到堆疊對於解決問題也很重要。可以參考Error最佳實踐。比如列印資訊:

--- FAIL: TestDigest (0.00s)
    digest_test.go:45: digest, mock md5
        hash write data
        _/Users/winlin/git/test/utility.digest
            /Users/winlin/git/test/utility.go:46
        _/Users/winlin/git/test/TestDigest
            /Users/winlin/git/test/digest_test.go:42
        testing.tRunner
            /usr/local/Cellar/go/1.8.1/libexec/src/testing/testing.go:657
        runtime.goexit
            /usr/local/Cellar/go/1.8.1/libexec/src/runtime/asm_amd64.s:2197

測試程式碼:

func TestDigest(t *testing.T) {
    if _, err := digest(nil, &mockMD5Write{md5.New()}); err == nil {
        t.Error("should failed")
    } else {
        t.Errorf("digest, %+v", err)
    }
}

當然這個地方是主動把error列印出來,因為用例就是應該要返回錯誤的,一般情況是:

func TestXXX(t *testing.T) {
    if err := pfn(); err != nil {
        t.Errorf("failed, %+v", err)
    }
}

這樣就可以知道堆疊了。

相關文章