触发事件

Vue 组件最常做的一件事情就是监听来自用户的输入了。vue-test-utils 和 Jest 让测试输入变得简单。让我们看看如何用 trigger 和 Jest mocks 来验证我们的组件是工作正常的吧。

本页中所描述的测试源码可以在 这里模拟用户输入 - 图1 找到。

创建组件

我们将创建一个简单的表单组件 <FormSubmitter>,包含一个 <input> 以及一个 <button>。当点击按钮时,有些事情应该发生。第一个例子会简单地显示一个成功消息,而后我们将继续写一个更有趣的例子来将表单提交到外部端点。

创建一个 <FormSubmitter> 并编写模板:

  1. <template>
  2. <div>
  3. <form @submit.prevent="handleSubmit">
  4. <input v-model="username" data-username>
  5. <input type="submit">
  6. </form>
  7. <div
  8. class="message"
  9. v-if="submitted"
  10. >
  11. Thank you for your submission, {{ username }}.
  12. </div>
  13. </div>
  14. </template>

当用户提交表单时,我们会显示一条感谢消息。我们想要异步的提交表单,所以使用了 @submit.prevent 以避免默认动作,也就是直接刷新页面。

现在添加提交逻辑:

  1. <script>
  2. export default {
  3. name: "FormSubmitter",
  4. data() {
  5. return {
  6. username: '',
  7. submitted: false
  8. }
  9. },
  10. methods: {
  11. handleSubmit() {
  12. this.submitted = true
  13. }
  14. }
  15. }
  16. </script>

太简单了,我们只是在表单提交时把 submitted 设置为 true,继而包含提示信息的 <div> 显示出来而已。

编写测试

让我们来看看测试。我们将该测试标记为 async — 读下去看看是为什么吧。

  1. import { shallowMount } from "@vue/test-utils"
  2. import FormSubmitter from "@/components/FormSubmitter.vue"
  3. describe("FormSubmitter", () => {
  4. it("reveals a notification when submitted", async () => {
  5. const wrapper = shallowMount(FormSubmitter)
  6. wrapper.find("[data-username]").setValue("alice")
  7. wrapper.find("form").trigger("submit.prevent")
  8. await wrapper.vm.$nextTick()
  9. expect(wrapper.find(".message").text())
  10. .toBe("Thank you for your submission, alice.")
  11. })
  12. })

这个测试相当地具有自解释特性了。我们 shallowMount 了组件,设置了 username 并使用了 vue-test-utils 提供的 trigger 方法以简化用户输入。trigger 对自定义事件起作用,也包括使用了修饰符的事件,如 submit.preventkeydown.enter 等等。

注意在调用 trigger 之后,我们写了 await wrapper.vm.$nextTick()。这就是为什么我们要将测试标记为 async-- 这样才能使用 await。从 vue-test-utils beta 28 起,你需要调用 nextTick 以确保 Vue 的反应式系统更新 DOM。有些时候你不调用 nextTick 也能侥幸成功,但如果你的组件开始变得复杂,就有可能遇到竞态条件从而让断言在 Vue 更新好 DOM 之前运行。更多的请读读官方文档 vue-test-utils documentation模拟用户输入 - 图2

以上测试同样遵循了单元测试的三个步骤:

  1. 安排(Arrange):为测试做好设置。在我们的用例中,是渲染了组件
  2. 行动(Act):对系统执行操作
  3. 断言(Assert):确保真实的结果匹配你的期望

在上面的测试中我们将每个步骤以一个空行隔开,从而让测试更易读。

yarn test:unit 运行测试。测试将会通过。

触发很简单 — 使用 find 取得你想要模拟一些输入的元素,并用事件名和任何修饰符调用 trigger 即可。

一个真实的例子

表单通常被提交到某些端点。我们来看看如何测试有着一个不同的 handleSubmit 实现的的组件。一种通常的实践是为你的 HTTP 库设置一个 Vue.prototype.$http 的别名。这使得我们要发起一次 ajax 只需调用 this.$http.get(...) 就行了。关于这种实践的更多可以参阅 这里模拟用户输入 - 图3

常用的 HTTP 库是 axios,一个流行的 HTTP 客户端。在本例中,我们的 handleSubmit 看起来可能会是这样的:

  1. handleSubmitAsync() {
  2. return this.$http.get("/api/v1/register", { username: this.username })
  3. .then(() => {
  4. // show success message, etc
  5. })
  6. .catch(() => {
  7. // handle error
  8. })
  9. }

在对应的测试用例中,用到的一项技术是 mockthis.$http 以创建符合期望的测试环境。可以阅读 mocks 加载选项的 文档模拟用户输入 - 图4 了解更多。让我们看看 http.get 方法的一种 mock 实现:

  1. let url = ''
  2. let data = ''
  3. const mockHttp = {
  4. get: (_url, _data) => {
  5. return new Promise((resolve, reject) => {
  6. url = _url
  7. data = _data
  8. resolve()
  9. })
  10. }
  11. }

这里发生了一些有意思的事情:

  • 我们创建了一个 url 变量和一个 data 变量以存储 urldata 并把它们传递给 $http.get。这对于断言请求以正确的 payload 命中了正确的端点是很有用的。
  • 在复制了 urldata 参数之后,我们立即 resolve 了 Promise,以模拟一次成功的 API 响应。

在测试之前,先看看新的 handleSubmitAsync 函数:

  1. methods: {
  2. handleSubmitAsync() {
  3. return this.$http.get("/api/v1/register", { username: this.username })
  4. .then(() => {
  5. this.submitted = true
  6. })
  7. .catch((e) => {
  8. throw Error("Something went wrong", e)
  9. })
  10. }
  11. }

同时,更新使用了新版本 handleSubmitAsync 函数的 <template>

  1. <template>
  2. <div>
  3. <form @submit.prevent="handleSubmitAsync">
  4. <input v-model="username" data-username>
  5. <input type="submit">
  6. </form>
  7. <!-- ... -->
  8. </div>
  9. </template>

现在只剩下测试了。

mock 一个 ajax 调用

首先在头部包含 mock 版本的 this.$http,在 describe 块之前:

  1. let url = ''
  2. let data = ''
  3. const mockHttp = {
  4. get: (_url, _data) => {
  5. return new Promise((resolve, reject) => {
  6. url = _url
  7. data = _data
  8. resolve()
  9. })
  10. }
  11. }

添加测试,将 mock 的 $http 传入 mocks 加载选项:

  1. it("reveals a notification when submitted", () => {
  2. const wrapper = shallowMount(FormSubmitter, {
  3. mocks: {
  4. $http: mockHttp
  5. }
  6. })
  7. wrapper.find("[data-username]").setValue("alice")
  8. wrapper.find("form").trigger("submit.prevent")
  9. expect(wrapper.find(".message").text())
  10. .toBe("Thank you for your submission, alice.")
  11. })

现在,不用管 Vue.prototype.$http 代表的是哪种真实的 HTTP 库,用到的都将是 mock 版本。这是很好的 — 我们可以控制测试环境并取得一致的结果。

运行 yarn test:unit 实际上将产生报错信息:

  1. FAIL tests/unit/FormSubmitter.spec.js
  2. FormSubmitter reveals a notification when submitted
  3. [vue-test-utils]: find did not return .message, cannot call text() on empty Wrapper

所发生的正是测试 早于mockHttp 返回的 promise 完成了。我们可以这样将测试变为异步的:

  1. it("reveals a notification when submitted", async () => {
  2. // ...
  3. })

只是这样的话,测试仍将早于 promise 完成。一种解决办法是使用 flush-promises模拟用户输入 - 图5,一个立即 resolve 所有 pending 中的 promise 的简单 Node.js 模块。用 yarn add flush-promises 安装它,并更新测试如下:

  1. import flushPromises from "flush-promises"
  2. // ...
  3. it("reveals a notification when submitted", async () => {
  4. const wrapper = shallowMount(FormSubmitter, {
  5. mocks: {
  6. $http: mockHttp
  7. }
  8. })
  9. wrapper.find("[data-username]").setValue("alice")
  10. wrapper.find("form").trigger("submit.prevent")
  11. await flushPromises()
  12. expect(wrapper.find(".message").text())
  13. .toBe("Thank you for your submission, alice.")
  14. })

使用 flush-promises 有个好的副作用,那就是能确保包括 nextTick 在内的所有 promises 都被 resolve,并且 Vue 也会更新 DOM。

现在测试通过了。flush-promises 的源码不超过 10 行,如果你对 Node.js 有兴趣那是值得一读并理解其工作原理的。(译注:主要就是利用 setImmediate 或 setTimeout 触发微任务队列的清空;个人觉得 这里模拟用户输入 - 图6 的做法可能更好一些)

我们同样要确保端点和 payload 的正确。向测试中添加两句断言:

  1. // ...
  2. expect(url).toBe("/api/v1/register")
  3. expect(data).toEqual({ username: "alice" })

测试仍然通过了。

总计

在本章中,我们看到了如何:

  • 在事件上使用 trigger,包括使用了诸如 prevent 修饰符的那些
  • 使用 setValue 设置一个使用了 v-model<input> 的值
  • 使用三步法编写单元测试
  • 使用 mocks 加载选项 mock 掉 Vue.prototype 上的方法
  • 如何用 flush-promises 立即 resolve 所有 promises

本页中所描述的测试源码可以在 这里模拟用户输入 - 图7 找到。