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,所有函数和数据都可以访问。
  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)
    }
}

这样就可以知道堆栈了。

0 个评论

要回复文章请先登录注册