为什么用 stub ?

当编写测试时,我们经常会想要 stub 掉代码中那些我们不感兴趣的部分。一个 stub 就是简单的一段替身代码。比方说你正在为 <UserContainer> 组件编写一个测试。该组件看起来就像这样:

<UsersDisplay> 有一个 created 像这样的生命周期方法:

  1. created() {
  2. axios.get("/users")
  3. }

我们想编写一个测试来断言 <UsersDisplay> 已经被渲染了。

axios 被用来在 created 钩子中生成一个指向外部服务的 ajax 请求。这意味着当你执行 mount(UserContainer) 时,<UsersDisplay> 也加载了,并且 created 启动了一个 ajax 请求。因为这是一个单元测试,我们只关心 <UserContainer> 是否正确的渲染了 <UsersDisplay> — 至于检验 ajax 请求在适当的点被触发,等等,则是 <UsersDisplay> 的职责了,应该在 <UsersDisplay> 的测试文件中进行。

一种防止 <UsersDisplay> 启动 ajax 请求途径是将该组件 stubbing(译注:插入替换的桩代码) 掉。让我们编写自己组件并测试,以获得关于使用 stubs 不同方式和优点的更好理解。

  1. <template>
  2. <ComponentWithAsyncCall />
  3. </template>
  4. <script>
  5. import ComponentWithAsyncCall from "./ComponentWithAsyncCall.vue"
  6. name: "ParentWithAPICallChild",
  7. components: {
  8. ComponentWithAsyncCall
  9. }
  10. </script>

<ParentWithAPICallChild> 是个简单的组件。其唯一的职责就是渲染 <ComponentWithAsyncCall><ComponentWithAsyncCall>,如其名字所暗示的,使用 axios http 客户端发起一个 ajax 调用:

<ComponentWithAsyncCall>created 生命周期钩子中调用了 makeApiCall

使用 mount 编写一个测试

让我们从编写一个验证 <ComponentWithAsyncCall> 是否被渲染的测试开始:

  1. import { shallowMount, mount } from '@vue/test-utils'
  2. import ParentWithAPICallChild from '@/components/ParentWithAPICallChild.vue'
  3. import ComponentWithAsyncCall from '@/components/ComponentWithAsyncCall.vue'
  4. describe('ParentWithAPICallChild.vue', () => {
  5. it('renders with mount and does initialize API call', () => {
  6. const wrapper = mount(ParentWithAPICallChild)
  7. expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
  8. })

运行 yarn test:unit 会产生:

  1. PASS tests/unit/ParentWithAPICallChild.spec.js
  2. console.log src/components/ComponentWithAsyncCall.vue:17
  3. Making api call

测试通过了 — 这很棒!但是,我们可以做得更好。注意测试输出中的 console.log — 这来自 makeApiCall 方法。理想情况下我们不想在单元测试中发起对外部服务的调用,特别是当其从一个并非当前主要目标的组件中发起时。我们可以使用 stubs 加载选项,在 vue-test-utils 文档的 中有所描述。

运行 yarn test:unit 时该测试将通过,而 console.log 也无影无踪了。这是因为向 stubs 传入 [component]: true 后用一个 stub 替换了原始的组件。外部的接口也照旧(我们依然可以用 find 选取,因为 find 内部使用的 name 属性仍旧相同)。诸如 makeApiCall 的内部方法,则被不做任何事情的伪造方法替代了 — 它们被 “stubbed out” 了。

也可以指定 stub 所用的标记语言,如果你乐意:

  1. const wrapper = mount(ParentWithAPICallChild, {
  2. stubs: {
  3. ComponentWithAsyncCall: "<div class='stub'></div>"
  4. }
  5. })

shallowMount 的自动化 stubbing

不同于使用 mount 并手动 stub 掉 <ComponentWithAsyncCall>,我们可以简单的使用 shallowMount,它默认会自动 stub 掉任何其他组件。用了 shallowMount 的测试看起来是这个样子的:

  1. it('renders with shallowMount and does not initialize API call', () => {
  2. const wrapper = shallowMount(ParentWithAPICallChild)
  3. expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
  4. })

运行 yarn test:unit 没有显示任何 console.log,并且测试也通过了。shallowMount 自动 stub 了 <ComponentWithAsyncCall>。对于有若干子组件、可能也会触发很多诸如 createdmounted 生命周期钩子行为的组件,使用 shallowMount 测试会很有用。我倾向于默认使用 shallowMount,触发有好使用 mount 的理由。这取决于你的用例,以及你在测试什么。

  • stubs 在屏蔽子组件中与当前单元测试无关行为方面很有用
  • 可以向默认 stub 中传入 true 或自定义的实现

可以在 这里stub 组件 - 图1 找到本页中描述的测试.