# stub 组件

可以在 这里 找到本页中描述的测试.

# 为什么用 stub ?

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

<UserContainer>
  <UsersDisplay />
</UserContainer>

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

created() {
  axios.get("/users")
}

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

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

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

# 创建组件

这个例子将使用两个组件。第一个是 ParentWithAPICallChild,用来简单地渲染另一个组件:

<template>
  <ComponentWithAsyncCall />
</template>

<script>
import ComponentWithAsyncCall from "./ComponentWithAsyncCall.vue"

export default {
  name: "ParentWithAPICallChild",

  components: {
    ComponentWithAsyncCall
  }
}
</script>

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

<template>
  <div></div>
</template>

<script>
import axios from "axios"

export default {
  name: "ComponentWithAsyncCall",
  
  created() {
    this.makeApiCall()
  },
  
  methods: {
    async makeApiCall() {
      console.log("Making api call")
      await axios.get("https://jsonplaceholder.typicode.com/posts/1")
    }
  }
}
</script>

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

# 使用 mount 编写一个测试

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

import { shallowMount, mount } from '@vue/test-utils'
import ParentWithAPICallChild from '@/components/ParentWithAPICallChild.vue'
import ComponentWithAsyncCall from '@/components/ComponentWithAsyncCall.vue'

describe('ParentWithAPICallChild.vue', () => {
  it('renders with mount and does initialize API call', () => {
    const wrapper = mount(ParentWithAPICallChild)

    expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
  })
})

运行 yarn test:unit 会产生:

PASS  tests/unit/ParentWithAPICallChild.spec.js

console.log src/components/ComponentWithAsyncCall.vue:17
  Making api call

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

# 使用 stubs 去 stub <ComponentWithAsyncCall>

让我们更新测试,这次 stubbing 掉 <ComponentWithAsyncCall>

it('renders with mount and does initialize API call', () => {
  const wrapper = mount(ParentWithAPICallChild, {
    stubs: {
      ComponentWithAsyncCall: true
    }
  })

  expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
})

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

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

const wrapper = mount(ParentWithAPICallChild, {
  stubs: {
    ComponentWithAsyncCall: "<div class='stub'></div>"
  }
})

# shallowMount 的自动化 stubbing

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

it('renders with shallowMount and does not initialize API call', () => {
  const wrapper = shallowMount(ParentWithAPICallChild)

  expect(wrapper.find(ComponentWithAsyncCall).exists()).toBe(true)
})

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

# 总结

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

可以在 这里 找到本页中描述的测试.