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