发布于 2026-01-05 10 阅读
0

用 JS 解释 mock 和 stub 的区别

用 JS 解释 mock 和 stub 的区别

桩(Stubs)和模拟(Mocks)是测试中两个基础概念,但经常被误解。事实上,马丁·福勒(Martin Fowler)专门撰写了一篇关于此主题的著名文章,Stack Overflow 上也有大量相关问题。
马丁的文章篇幅较长,对于现在缺乏耐心的读者来说可能有点冗长,而且容易跑题,也没有使用当前热门语言 JavaScript 的示例。
我将尽量用更简洁的方式进行解释。

我先从标题定义开始:
桩对象和模拟对象都是用于测试的虚拟对象,桩对象只实现预先编程的响应,而模拟对象还预先编程了特定的预期。

将其融入工作流程:

存根

  • 设置- 定义桩对象本身,即要桩化程序中的哪个对象以及如何桩化。
  • 练习——运行您想要测试的功能
  • 验证——检查存根中运行过的值,确认它们符合预期。
  • 清理工作——如有必要,进行清理。例如,时间桩通常是全局的,你需要将控制权交还给全局变量。

模拟

  • 设置对象- 定义模拟对象,包括要模拟的对象以及模拟方式(类似于桩对象)。
  • 设置预期——定义你预期这个模拟对象在内部会发生什么。
  • 练习——运行您想要测试的功能
  • 验证模拟对象- 验证模拟对象的预期是否得到满足。在某些 JavaScript 库中,这无需额外调用即可自动完成,模拟对象的预期会自行验证,并在必要时抛出异常。(主要用于异步测试)。
  • 验证——验证对模拟结果的任何其他预期。
  • 拆卸——如有必要,进行清理。

JS 社区中的模拟对象和存根

在开始编写代码之前,由于我的示例将使用 JS,所以这里有一个重要的注意事项。

Jest是 JS 社区中最成功的测试库之一。但我不会在我的示例中使用它,原因很简单:Jest 有其自身的局限性,并且没有实现模拟对象(mock)。该库中
所谓的“模拟对象” mock,实际上根据定义就是一个桩对象(stub)。你不能直接对模拟对象本身进行预期,而只能观察它的行为,并根据其行为来设定预期。

sinon.js我将使用同时实现了模拟对象和桩对象概念的方法来演示这个概念。

存根示例

在本例中,我们将对一个虚拟的电商网站商品购买功能进行单元测试。我们将尝试付款并获取付款状态,如果付款成功,我们将发送一封邮件。

const purchaseItemsFromCart(cartItems, user) => {
  let payStatus = user.paymentMethod(cartItems)
  if (payStatus === "success") {
    user.sendSuccessMail()
  } else {
    user.redirect("payment_error_page")
  }
}

}
"when purchase payed successfully user should receive a mail" : function() {
  // Setup
  let paymentStub = sinon.stub().returns("success")
  let mailStub = sinon.stub()
  let user = {
    paymentMethod: paymentStub,
    sendSuccessMail: mailStub
  }

  // Exercise
  purchaseItemsFromCart([], user)

  // Verify
  assert(mailStub.called)
}

模拟示例

现在让我们用模拟对象做同样的事情。

"when purchase payed successfully user should receive a mail" : function() {
  // Setup objects
  let userMock = sinon.mock({
    paymentMethod: () => {},
    sendSuccessMail: () => {}
  })

  // Setup expectations
  userMock.expect(paymentMethod).returns("success")
  userMock.expect(sendSuccessMail).once()

  // Exercise
  purchaseItemsFromCart([], user)

  // Verify mocks
  userMock.verify()
}

太好了。我应该在什么情况下使用它们?

现在,这才是真正有趣的问题。对此也有很多争论——背后的开发者决定不实现经典的模拟功能
是有原因的。jest

模拟对象可以做到桩对象能做的一切,还能直接对它们模拟的对象设置预期。
这会导致大型测试的可读性问题,并且容易导致测试中出现预期和测试对象并非测试唯一目的的情况,从而使测试变成过于关注内部机制的白盒测试。

Sinon 的文档中甚至将这一点作为何时使用 mock 对象的指导原则:

Mocks should only be used for the method under test.
In every unit test, there should be one unit under test.

因此,为了避免过度使用此功能并创建令人困惑甚至可能用途错误的测试,您应该将测试中模拟对象的使用限制为一个对象。

或者,你知道,你也可以直接使用 Jest,它从一开始就没有实现这类模拟,从而让你无需做出这样的决定。

文章来源:https://dev.to/snird/the-difference- Between-mocks-and-stubs-explained-with-js-kkc