GOLANG最容易做测试MOCK

测试时,一些底层的库非常难以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,所有函数和数据都可以访问。 1. 有些函数非常难以出错,但是不代表不出错,比如这里的`Write`方法,一般都是不会有问题的,但是测试如果覆盖不到,保不齐哪天跑到这一行就挂掉了。 1. MOCK桩对象或者函数,如果总是要把目标全部实现一遍,比如hash这个接口有5个方法,对`Write`打桩时只需要覆盖这个函数,其他的可以不动。是的,聪明的你可能会想到继承,但是如果这个类是隐藏的呢?比如一个md5的实现是隐藏不能访问的,暴露的只有hash的接口,怎么从md5这个类继承呢?GOLANG提供了类似从实现了接口对象的接口继承的方式,实际上是组合,具体看下面的实现。 1. 有些古怪的逻辑,比如这里判断摘要是16字节,一般情况下也不会出现错误,当然utest也必须得覆盖到,万一哪天用了一个hash算法跑到这个地方,不能出现问题。 > Remark: 注意到这个地方用了一个`errors`的package,它可以打印出问题出现的堆栈,参考[Error最佳实践](https://gocn.io/article/348). 用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最佳实践](https://gocn.io/article/348)。比如打印信息: ``` --- 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) } } ``` 这样就可以知道堆栈了。

0 个评论

要回复文章请先登录注册