如何在测试中更好地使用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 类型。若是你这样作了,你会发现你的测试运行速度更快,更稳定,而且还会有更高的测试覆盖率,你的软件架构设计也会愈来愈好。

参考文档

相关文章
相关标签/搜索