Vue.js 应用测试
书籍链接
测试概览
- 回归测试:检查之前的特性是否仍正常工作。
- 端到端测试:从用户的视角通过浏览器自动检查应用程序是否正常工作。flaky 测试表示即使被测应用程序正常运行,测试仍然频繁失败,或许是因为代码执行时间太长或许是因为 API 暂时失效。
- 单元测试:对应用程序最小的部分(单元)运行测试的过程。通常,测试的单元是函数,但在 Vue 应用程序中,组件也是被测单元。
- 快照测试:传统快照测试是在浏览器中启动应用程序并获取渲染页面的屏幕截图。Jest 快照测试可以对 JavaScript 中任何可序列化值进行对比。
测试原则
- 如果测试的功能没有发生改变,测试就不应该中断。
- 避免 Boolean 断言
- 通过比较 Boolean 值完成 Boolean 断言。当断言失败时,“期望值为 true,返回值为 false”这样的错误消息并不能清楚说明测试为什么失败。
- 替代 Boolean 断言的方法是使用富有表达力的值断言。顾名思义,值断言是将一个值与另一个值进行比较的断言。
- 最小化模拟原则 通常在测试环境中,你需要将模拟数据传递给组件或函数。而在生产环境中,这个数据可能是具有许多属性的庞大对象。庞大对象使得测试更复杂难读,你应始终传递测试所需的最少数据。
- 你需要手动测试应用程序中的 HTML 和 CSS 样式是否正确。单元测试会阻碍这个过程。
- 测试组件输出时应该记住以下原则
- 仅测试动态生成的输出。
- 仅测试组件契约部分的输出。
作者认为的构建前端 Vue 应用程序测试套件的最佳方式
测试驱动开发(TDD)
- 在组件中编写代码之前,你需要先编写能够确保组件正常运行的测试代码。
- “红、绿、重构”是一种很流行的 TDD 方法。红代表编写一个不能通过的测试,绿代表让测试通过,在测试通过后,通过重构增强代码可读性。
作者编写一个 Vue 组件的顺序:
- 确定需要编写的组件。
- 为每个组件编写单元测试和源代码。
- 调整组件的样式。
- 为已完成的组件添加快照测试。
- 在浏览器中手动测试代码。
- 编写端到端测试。
代码覆盖率:度量自动化测试运行代码库中代码行数的一个指标。通常,代码覆盖率以百分比来度量:100%代码覆盖率意味着在执行测试期间每行代码都会被运行,0%代码覆盖率意味着未执行任何代码行。
单元测试组件
组件契约:一个好的组件单元测试应该始终可以触发一个输入,并断言组件产生正确的输出。 组件常见的输入:
- 组件中的 props
- 用户操作(如点击按钮)
- Vue 事件
- Vuex store 中的数据
组件输出:
- 发射出的事件
- 外部函数的调用
创建你的第一个测试
Jest
- 使用.spec.js 文件扩展名编写测试。这里 spec 代表规格说明,因为单元测试是规范代码行为的。当被测函数被调用时,每个测试都会对其指定一个预期结果。
- 将单元测试放置在尽可能接近被测代码的位置,这样测试文件会更容易被其他开发人员找到。
- 在 Jest 中使用 test 函数定义单元测试。test 函数有两个参数:第一个参数是一个字符串,用于标识测试报告中的测试。当你运行 Jest 时,就会明白我指的测试报告的含义。第二个参数是包含测试代码的函数。Jest 解析测试文件中的每个测试函数,并运行测试代码,最终返回测试结果报告。
- Jest 可以显式运行,也可以在监控模式下运行。监控模式可以监控文件变更并重新运行更新后的文件。
- 你可以通过--watch 标志位调用 Jest 来启动监视模式。
- 如果你的测试脚本中有--no-cache 标志位,则 watch 会重新运行所有测试。
- 组织单元测试的一种方法是使用 describe 函数。describe 函数将一个单元测试套件定义为一个测试套件。当你在命令行运行测试时,Jest 会格式化输出,以便你了解哪些测试套件通过,哪些测试套件失败。describe 函数中的代码称为 describe 代码块。避免深度嵌套 descirbe 函数,尽可能扁平化 describe 函数。
- babel-jest:可以将现代 JavaScript 编译成可以在 Node 中运行的 JavaScript。
- vue-jest:可以将单文件组件(SFC)编译成 JavaScript。
expect
- 返回值是一个 Jest 匹配器对象。
expect.toBe(value)
- 使用严格相等比较(===)来检查值的相等性。
expect.toEqual(value)
- 递归比较对象和数组的值。它适用于比较复杂的数据结构和对象。不会比较两个对象的引用。
expect.toContain(item)
- 检查一个值是否包含在它所检查的字符串中的某个位置。
expect.toHaveLength(number)
- 检查当测试具有 length 属性的数组或类数组对象时,你可以使用 toHaveLength 匹配器。
expect.assertions(number)
- expect.assertions(number)验证测试期间是否调用了一定数量的断言。这在测试异步代码时通常很有用,以确保回调中的断言确实被调用。这在测试异步代码时通常很有用,以确保回调中的断言确实被调用。
jest.useFakeTimers(implementation?: 'modern' | 'legacy')
- 指示 Jest 使用标准计时器函数的假版本(setTimeout、setInterval、clearTimeout、clearInterval、nextTick 和 setImmediate)。clearImmediateDate
- 如果您'legacy'作为参数传递,则将使用 Jest 的遗留实现,而不是基于@sinonjs/fake-timers.
- 返回 jest 对象进行链式操作。
jest.advanceTimersByTime(msToRun)
- 仅执行宏任务队列(即按 setTimeout()或 setInterval()和排队的所有任务 setImmediate())。
- 是异步的,需要添加 await
- 通过调用 msToRun 这个 API 时,所有定时器都将提前毫秒。当调用这个 API 时,所有定时器都会提前 msToRun 毫秒。
jest.spyOn(object, methodName, accessType?)
- 创建一个类似于 jest.fn 的模拟函数,还可以跟踪对 object[methodName]的调用。返回一个 Jest 模拟函数。
- 从 Jest 22.1.0+ 开始,该 jest.spyOn 方法采用可选的第三个参数,accessType 该参数可以是'get'或'set',当您想要分别监视 getter 或 setter 时,这被证明是有用的。
expect.toHaveBeenCalledWith(arg1, arg2, ...)
- 也在别名下:.toBeCalledWith()
- 用于.toHaveBeenCalledWith 确保使用特定参数调用模拟函数。使用相同的算法检查参数.toEqual。
jest.fn(implementation?)
返回一个新的、未使用的模拟函数。可以选择进行模拟实现。
jest.toHaveBeenCalledTimes(number)
- 也在别名下:.toBeCalledTimes(number)
- 确保模拟函数被调用的确切次数。
jest.mock(moduleName, factory, options)
- 在需要时使用自动模拟版本来模拟模块。factory 并且 options 是可选的。
mockFn.mockReturnValueOnce(value)
- 接受一次调用模拟函数时返回的值。可以链接起来,以便对模拟函数的连续调用返回不同的值。当没有更多 mockReturnValueOnce 值可供使用时,调用将返回指定的值 mockReturnValue。
mockFn.mockResolvedValue(value)
- 用于模拟一个异步函数的返回值,并设定返回的解决值为指定的 value。
mockFn.mockResolvedValueOnce(value)
- 模拟一个异步函数调用的返回值,并且只设定一次调用的返回值为指定的 value。
mockFn.mockRejectedValue(value)
- 模拟一个异步函数调用的返回值,并设置返回的拒绝值为指定的 value。
mockFn.mockRejectedValueOnce(value)
- 模拟一个异步函数调用的返回值,只设定一次调用的返回拒绝值为指定的 value。
mockFn.mockImplementation(fn)
- 接受应该用作模拟实现的函数。模拟本身仍然会记录所有进入自身的调用和来自自身的实例——唯一的区别是,当调用模拟时,实现也会被执行。
mockFn.mockRestore()
- 执行所有 mockFn.mockReset()操作,并恢复原始(非模拟)实现。当您想要在某些测试用例中模拟函数并在其他测试用例中恢复原始实现时,这非常有用。
expect.toMatchSnapshot(propertyMatchers?, hint?)
- 这可确保值与最新快照匹配。
挂载组件
当你导入一个已编译的 Vue 组件时,它只是一个带有一个渲染函数和一些属性的对象(或函数)。要测试组件行为是否正确,你需要启动它并开启渲染过程。用 Vue 的说法,就是你需要挂载组件。
Vue Test Utils
mount
- 该方法在接收一个组件后,会将其挂载并返回一个包含被挂载组件实例(vm)的包装器对象。理解包装器对象的最佳方法是与它进行交互。
- mount 返回的包装器不仅包含 Vue 实例,还包括一些辅助方法,你可以使用它们来设置 props,检查实例属性以及对实例执行操作。
shallowMount
- shallowMount 不会像 mount 一样渲染整个组件树,它只渲染一层组件树。
- shallowMount 可以确保你对一个组件进行独立测试,有助于避免测试中因子组件的渲染输出而混乱结果。
- 当你挂载组件时,可以使用 Vue Test Utils 将 prop 作为一个选项对象传递给组件。
find
- 返回匹配选择器的第一个 DOM 节点。可以使用任何有效的 DOM 选择器 (使用 querySelector 语法)。
findComponent
- 返回第一个匹配的 Vue 组件的 Wrapper。
attributes
- 返回 Wrapper DOM 节点的特性对象。如果提供了 key,则返回这个 key 对应的值。
findAll
- 返回一个 WrapperArray。可以使用任何有效的选择器。
findAllComponents
- 为所有匹配的 Vue 组件返回一个 WrapperArray。
props
- 返回 Wrapper vm 的 props 对象。如果提供了 key,则返回这个 key 对应的值。
classes
- 返回 Wrapper DOM 节点的 class。
- 返回 class 名称的数组。或在提供 class 名的时候返回一个布尔值。
element
- (只读):包裹器的根 DOM 节点
vm
- Component (只读):这是该 Vue 实例。你可以通过 wrapper.vm 访问一个实例所有的方法和属性。这只存在于 Vue 组件包裹器或绑定了 Vue 组件包裹器的 HTMLElement 中。
wrapper.exists
- 断言 Wrapper 或 WrapperArray 是否存在。
wrapper.emitted
- 返回一个包含由 Wrapper vm 触发的自定义事件的对象。
wrapper.trigger
- 在该 Wrapper DOM 节点上异步触发一个事件。
- trigger 带有一个可选的 options 对象。options 对象内的属性会被添加到事件上。trigger 会返回一个 Promise,当这个 Promise 被解决时,会确保组件已经被更新。
wrapper.setValue
- 设置一个文本控件或 select 元素的值并更新 v-model 绑定的数据。
wrapper.setChecked
- 设置 checkbox 或 radio 类
<input>
元素的 checked 值并更新 v-model 绑定的数据。
createLocalVue
- createLocalVue 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
- 在组件渲染功能和观察者期间,errorHandler 选项可用于处理未捕获的错误。
- 可通过 options.localVue 来使用:
渲染组件输出测试
创建静态信息流的规范
- 就需求达成一致
- 回答有关细节的问题,以获得高级规范与设计
- 将设计分解到组件级
- 编写组件级规范
测试范围
- 渲染文本测试(组件文本内容,元素的文本内容)
- 测试 DOM 属性
- 测试渲染组件的数量
- 测试 prop
- 测试 class
- 测试样式
报错解决方式
在测试中,我们期望断言的内容是实际渲染的文本是否包含item.url
的值。在 Vue 中,props 的类型是通过组件的 prop 定义来确定的。当我们在测试中传入一个 item
对象,其中的 url
是数字类型时,Vue 会自动将其转换为字符串类型,并在渲染时以字符串的形式进行展示。因此,我们执行测试时需要考虑传入的值在渲染时会被转换为字符串类型,所以断言的内容也必须是字符串类型的。
测试组件方法
测试自包含的方法并不复杂。但是现实世界的方法通常具有依赖项,而测试有依赖的方法,会引入一个更复杂的环境。依赖是指在被测代码单元控制之外的任何代码。依赖有多种形式。浏览器方法、被导入模块和被注入的 Vue 实例属性...
测试公共组件和私有组件方法
- 私有方法:
- 它们一般不在组件外部被调用。私有方法是实现细节的,因此不用直接为它们编写测试。
- Vue 社区中通常使用下划线开头来表示私有方法。
- 可以使用其他标识符或者 ES6 中的 Symbol 类型来定义私有方法。
- 公有方法:
- 在 Vue 中,公共方法不是一种常见的模式,但它们可以很强大。你应该习惯为公共方法编写测试,因为公共方法是组件契约的一部分。
- 在 Vue 组件中,通常将 data、methods、computed 和 watch 等属性直接暴露给组件实例,这些方法可以被外部访问,并被视为组件的公有方法。
- 当我们说这些属性和方法可以被外部访问时,我们指的是在组件定义和使用的 scope 范围内,而不是在组件外部的其他模块或文件中。在组件之外的地方,这些属性和方法是不可直接访问的,除非组件显式地通过 prop、event 或者提供全局访问(比如 Vuex)的方式,将这些属性和方法暴露给外部。
- 测试公共方法的过程很简单:调用组件方法并断言方法调用正确地影响了组件输出。
测试定时器函数
- 在不减慢测试速度的情况下测试定时器函数的唯一方法是将定时器函数替换为同步运行的自定义函数。
- 当你调用 jest.useFakeTimers 方法时,Jest 假定时器会替换全局定时器函数来工作。定时器被替换后,你可以使用 runTimersToTime 推进假时间。
- 在测试套件中使用假定时器最安全的方法是在每次测试运行之前调用 useFakeTimers。这样,定时器函数将在每次测试之前复位。
- 你可以使用 beforeEach 钩子函数在每次测试之前运行函数。这个钩子函数对于执行测试设置很有用。
- j 通过 jest.advanceTimersByTime 提前定时器若干 ms。
使用 spy 测试
- 通常,当你测试的代码使用了你不能控制的 API 时,你需要检查 API 中的函数是否已被调用。例如,假设你正在浏览器中运行代码,并想要测试 window.clearInterval 是否被调用,你会怎么做?
- 一种方法就是使用 spy。很多库都有 spy 实现,但是因为你现在使用的是 Jest 的 kitchen sink,所以你可以用 jest.spyOn 函数创建一个 spy。
- spyOn 函数让你可以使用 toHaveBeenCalled 匹配器检查函数是否被调用。
- 如何测试函数是否是带参数被调用?使用 toHaveBeenCalledWith 匹配器测试 spy 是否带指定参数被调用。
- 如何知道应该用什么值调用 clearInterval?mockReturnValue 函数可以配置假定时器函数的返回值,因此你可以将 setInterval 返回值配置成任何一个你想要的值。
报错解决方式
- 使用 async 和 await 关键字来确保 wrapper.vm.$nextTick() 方法在组件更新后再执行断言的代码。通过这种方式,我们可以确保在调用 start 方法后,组件已经更新,并且可以访问到修改后的状态。
- 测试 spy 时,Jest27 需要对 clearInterval 跟 setInterval 都进行 spyOn 监听,且通过 jesy.spyOn 链接调用的方式设置 setInterval 的 mockReturnValue。具体查看
ProgressBar.spec.js
。
模拟代码
模拟代码是用你可控制的代码替换你不可控制的代码。
好处
- 你可以在测试中停止类似 HTTP 调用这样的副作用问题。
- 你可以控制函数的行为和返回值。
- 你可以测试函数是否被调用。
模拟 Vue 实例属性
- 漏桶问题:当你的组件使用 Vue 实例属性时,它就像一个带孔的桶。如果你在测试中挂载组件,你需要为其添加所需属性,否则桶将泄漏,你将收到错误。
- 可以使用 mocks 选项注入一个实例属性。mocks 选项可以使控制实例属性变得很轻松。你只要确保在运行测试之前,使用它来给漏洞打补丁。
- 如何测试$bar.start 函数是否被调用?实现的方法是使用 Jest mock 函数。
- 有时你需要在测试中检查函数是否被调用,那么你可以使用可记录自身信息的模拟函数替换该函数。
模拟模块依赖
- 当一个 JavaScript 文件导入另一个模块时,被导入的模块将成为一个模块依赖。大多数情况下,在单元测试中有模块依赖是好事,但是如果该模块依赖有副作用,比如发送 HTTP 请求,则可能会导致问题。
- 模拟模块依赖是将导入的模块替换为另一个对象的过程。
- HTTP 请求不在单元测试范围。它们会降低单元测试的速度,并且妨碍单元测试的可靠性(HTTP 请求永远不会 100%可靠)。
- Jest 提供了一个 API,用于选择当一个模块导入另一个模块时返回哪些文件或函数。要使用此功能,你需要创建一个 Jest 应该解析的 mock 文件,而不是被请求文件。mock 文件将包含你希望测试使用的函数,而不是真正的文件函数。
- 你可以通过将文件添加到mocks目录来创建一个 mock 文件,文件名称与你要模拟的文件名称相同。例如,要模拟 api.js 文件,你将创建一个导出 fetchListData mock 函数的 src/api/mocks/api.js 文件。
报错解决方式
- mocks下文件报错 jest 不存在,可以通过修改
.eslintrc.js
下的overrides
实现。
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
'**/__mocks__/*.{j,t}s?(x)', // 添加这一行
],
env: {
jest: true,
},
},
]
测试异步代码
- 在异步测试中设置断言数量的原因是为了确保在测试结束之前执行完所有断言。
- 告诉 Jest 使用你创建的 mock api 文件。
- 使用 flush-promises 库等待 1 挂机的 promise。
- 通过 mockFn.mockResolvedValueOnce(value)/mockFn.mockRejectedValueOnce()模拟数据请求结果。
适度使用 mock
- 控制一个函数的返回值,检查函数是否被调用,以及阻止 HTTP 请求这样的副作用。这些都是模拟的很好的例子,因为在没有模拟的情况下很难对它们进行测试。但是,模拟应该始终是最后的选择。
- 你应该只模拟副作用是减慢测试速度的文件。常见的减慢测试速度的副作用如下:
- HTTP 调用
- 连接数据库
- 使用文件系统
测试事件
测试原生 DOM 事件
合成事件
- 在 Vue Test Utils 中,每个包装器都有一个 trigger 方法,用于在包装元素上分发一个合成事件。
- 合成事件是在 JavaScript 中创建的事件。实际上,合成事件的处理方式与浏览器分发事件的方式相同。区别在于原生事件通过 JavaScript 事件循环异步调用事件处理程序,合成事件则是同步调用事件处理程序。
测试自定义事件
- 对于发射事件的组件,发射的事件是组件的输出。
- 对于监听自定义事件的父组件,发射的事件是组件的输入。
- 你可以使用 Vue Test Utils 的 emitted 方法测试组件是否发射事件。使用事件名称调用 emitted 以返回一个数组,该数组包含了每个发射事件的 payload。
报错解决
- wrapper.emitted 不需要参数,通过返回值.key 获取
测试输入表单
测试文本输入框
- 使用一个 input 的 value 编写测试时,你必须在触发测试输入之前手动设置 value 属性。
- 要更新一个文本输入的 v-model,你需要设置元素上的 value 值,然后在元素上触发一个变更事件以强制更新绑定值。与其依赖于 v-model 的内部实现,不如使用包装器的 setValue 方法,该方法可以对一个输入设值并使用新的值更新绑定数据。
测试单选按钮
- 测试单选按钮类似于测试输入表单。但单选按钮的内部 state 不是用 value 属性表示,而是用 checked 属性。
- 使用 setChecked 方法。
了解 jsdom 的局限性
- 理想情况下,你应该再添加一个测试来检查提交表单的行为是否会导致重新加载,但是使用 jsdom 是无法编写的。
- 在 jsdom 中,web 平台未实现的两大部分是:
- 布局:布局是关于计算元素位置的。如 Element.getBoundingClientRects 这样的 DOM 方法将不会按预期运行。在本书中你不会遇到任何与此相关的问题,但是如果你使用元素的位置来计算组件的样式,就可能会遇到。
- 导航:jsdom 中没有页面的概念,因此你无法创建请求并导航到其他页面上。
- 你需要编写一个端到端测试而不是单元测试,来检查一个表单提交动作是否会导致页面重新加载。
测试 Vuex
- 通过一个配置对象实例化一个 Vuex store。
- 为了保证代码模块化且易于理解,你还应该为 mutation、getter 和 action 也创建单独的文件。添加空方法的好处是可以避免类型错误。记住,当单元测试尝试调用未退出的方法时,测试将失败并返回一个无意义的类型错误。单元测试中的类型错误非常糟糕,它们无法证明测试是否因正确原因而失败。你应该在计划编写测试的文件中创建空方法和函数,以确保不会出现类型错误。
- 添加空方法的好处是可以避免类型错误。记住,当单元测试尝试调用未退出的方法时,测试将失败并返回一个无意义的类型错误。单元测试中的类型错误非常糟糕,它们无法证明测试是否因正确原因而失败。你应该在计划编写测试的文件中创建空方法和函数,以确保不会出现类型错误。
- 有一种测试 Vuex store 的方法是对 Vuex 的 store 组成部分分别进行测试。分别测试的好处是单元测试可以小且聚焦。当一个单元测试失败时,你将可以确切地知道 store 中的哪个部分出现了问题。
分别测试 Vuex store 的组成部分
颗粒状地测试 Vuex store 有一些很大的缺点。最大的问题是你经常需要模拟 Vuex 功能。就像你之前看到的那样,极限模拟会使得测试编写变得很困难并且可能引入 bug。
测试 mutation
- 要为一个 mutation 编写单元测试,你可以使用相同的参数调用该 mutation。先创建一个假 state 对象和 payload 对象,然后使用假 state 和 payload 对象调用该 mutation,并断言 state 对象已被变更为正确的值。
测试 Vuex getter
- 要测试 getter,可以使用一个假的 state 对象调用 getter 函数,该 state 对象包含 getter 将使用的值,然后断言 getter 是否返回你期望的结果。
- Hacker News 应用程序不使用链式 getter,但测试它们的技术类似于测试普通的 getter。区别在于链式 getter 接收一个其他 getter 的结果对象作为它们的第二个参数。你可以按照为 getter 编写测试的同样方式,为链式 getter 编写测试——使用一个假的 state 对象和一个假的 getter 对象调用 getter。
测试 Vuex action
- 要测试 action,你可以按照 Vuex 调用它的方式调用该函数,并断言该 action 是否按你的期望执行。通常,这意味着要利用模拟避免产生 HTTP 调用。
- 要测试 fetchListData action 是否正常运行,你需要使用极限模拟。换句话说,你将要在测试中添加许多 mock 功能,从而测试 action 函数是否使用正确的值调用了正确的方法。本章的最后,我将向你展示一种以较少的模拟来测试 action 的替代方法,但要独立测试 action,模拟是你唯一的选择。
- 极限模拟是指你在测试中模拟复杂的功能。极限模拟可能会很危险。你使用的 mock 越多,你的测试就越不准确。mock 不测试实际的功能,它们只是在测试假设的功能。同时 mock 也会使测试更难以理解和维护,测试也将变得更加昂贵。
测试一个 Vuex store 实例
- 单元测试中你最不希望看到的是测试之间的 mutation 泄漏。解决方案是通过克隆 store 配置对象删除任何对象引用。这样你可以继续使用基础的 store 配置对象,并且每次测试时都会有一个全新的 store。
- 在这些测试样例中,Vuex 是安装在 Vue 基础构造函数上的。在基础构造函数上安装插件会导致测试泄漏,因为将来的测试会使用被污染的 Vue 构造函数。要避免它们,你需要学习如何使用 localVue 构造函数。
- 在原则上这是可行的,但在实际测试中你通常需要安装 Vue 插件,这些插件可能对 Vue 基础构造函数进行更改。要安装插件并避免污染 Vue 基础构造函数,你可以使用 Vue Test Utils 创建的 localVue 构造函数。localVue 构造函数是一个从 Vue 基础构造函数扩展而来的 Vue 构造函数。你可以在 localVue 构造函数上安装插件,而不会影响 Vue 基础构造函数。
- localVue 构造函数是从 Vue 基础构造函数扩展而来的,因此 Vue 基础构造函数先前的任何更改都将被包含在 localVue 中。
- 测试 store 实例的好处是你可以避免模拟 Vuex,并且测试实现不是太具体。你可以重构 store 的内部,只要 store 维持它的契约,store-config 测试仍然会通过。
测试组件中的 Vuex
- 你可以使用以下两种方法的其中一种为测试中的一个组件提供一个 Vue store。第一种是创建一个 mock store 对象,并将其添加到带有 mocks 选项的 Vue 实例中。另一种方法是通过 Vuex 和模拟数据创建一个真实的 store 实例。这种方法更加健壮,因为你不需要重新编写 Vuex 功能。
- 任何安装插件的测试都应该使用一个 localVue 构造函数。
使用工厂函数组织测试
工厂函数是指那些返回新对象或者新实例(instance)(也被称为生成器)的函数。你可以将工厂函数添加到需要重复设置的测试中以删除冗余的代码。
- 你可以编写一个 createWrapper 函数来创建并返回带有 mocks 选项的包装器,而不是向每个 shallowMount 调用添加相同的 mocks 选项。
- 在测试中使用工厂函数有两个好处:
- 避免重复的代码
- 工厂函数为你提供一种可以沿用的模式
- 使用工厂函数所付出的代价是增加了代码中的抽象内容,这会使得测试让未来的开发人员更加难以理解。
- DRY 原则指出,如果在一个应用程序中多次编写了相似的代码,你应该将共用逻辑提取到函数或方法中,而不是在各代码库之间重复代码。
- 在 before each 模式中,你在 beforeEach 设置函数中的每个测试之前重写公共变量。这种方法避免了在测试中重复创建对象,是测试中常用的模式。
- 工厂函数存在的一个问题是没有保持对用作创建对象的属性的函数或对象的引用。
测试 Vue Router
测试路由属性
- 当 Vue Router 被安装到 Vue 上时,它会添加两个实例属性:$route属性和$router 属性。这些属性应该带有一个巨大的警告标志,因为它们可能会在测试中造成很多问题。$route和$router 作为只读属性被添加到了 Vue 实例中。在添加过后,它们的值将无法被重写。
- $route 属性包含了有关当前匹配路由的信息,其中包括路由参数中的任何动态字段。
- $router 实例包含了可以控制 Vue Router 的方法。
测试$route 属性
- 模拟 Vue router 实例属性的技巧与测试其他实例属性的技巧无异。你可以使用 Vue Test Utils 中的 mocks 挂载选项,将其添加为测试中的实例属性。
- 访问由 Vue Router 注入属性的组件有很多要修补的问题。你有两种方法可以给测试中的组件添加$route和$router。
- 首先,你可以使用 localVue 安装 Vue Router。如果你正在测试的组件需要访问$route和$router 上的属性和方法,而不需要在测试中使它们的值受控,那么这种方法很实用。
- 要控制$route和$router 对象中包含的数据,你需要使用 mocks 挂载选项。mocks 挂载选项使得属性在每个挂载组件内都可用。
测试$router 属性
- 由于$router是一个实例属性,因此你可以使用Vue Test Utils的mocks挂载项在测试中控制$router 的值。
避免常见的陷阱
- Vue Router 安装之后,$route和$router 属性也一并作为只读属性添加进了 Vue 构造函数原型。无论你做什么,Vue Router 的属性值被添加到 Vue 原型之后,都无法被覆盖。
测试 RouterLink 组件
- 要测试链接到其他页的 RouterLink 组件,你需要断言 RouterLink 组件接收到了正确的 to prop。
- Vue Router 并没有导出 RouterLink 和 RouterView 组件,因此无法把 RouterLink 当作选择器使用。解决方法是控制渲染成 RouterLink 的组件,把这个受控组件用作选择器。你可以使用 Vue Test Utils 控制已渲染组件。当 Vue 父组件渲染子组件时,Vue 会试图在父组件实例上对子组件进行解析。你可以使用 Vue Test Utils 的 stubs 选项覆盖这个过程。
- Vue Test Utils 可以输出一个表现就像 RouterLink 的 RouterLinkStub 组件。
- 推荐在包装器工厂函数里使用 stubs 挂载选项来存根这些组件,而不是在 localVue 构造函数中安装 Vue Router,以便在需要时对 Vue Router 属性进行覆盖。
Vuex 与 Vue Router 配合使用
- 在 Vuex store 中使用 Vue Router 的属性可能会很实用。你可以使用 vuex-router-sync 库同步 Vuex 和 Vuex Router,使 route 对象在 store 中也可以被获取到。
测试 mixin
- 测试 mixin 的过程很简单。在组件中注册 mixin,挂载组件,然后检查 mixin 是否产生了预期的行为。
- 你需要尽量精简测试代码,测试代码越少,就越容易理解。因此,在 mixin 测试中,创建具有所需的最少选项的组件,以此检查 mixin 是否工作正常。
- 在测试中编辑 document.title 的值会改变当前运行上下文中其他测试的 title 值(在 Jest 中,每个测试文件只运行于它自己的上下文之中)。
测试组件中的局部 mixin
- mixin 是一个具体实现。为组件编写的单元测试不必考虑 mixin 内部实现,只需测试 mixin 的输出即可。没有什么特殊的技巧去测试使用了 mixin 的组件,你需要编写一个测试检查组件的期望输出,然后使用 mixin 实现产生输出的功能。
测试组件中的全局 mixin
- 不论 mixin 是局部注册的还是全局注册的,你都可以使用相同的测试,因为输出是一样的。
- 在测试启动之前,你需要把测试中用到的 mixin 和过滤器添加到 Vue 构造函数中去。有一种情况你不能在全局添加 mixin 和过滤器,那就是它们产生了副作用,降低了测试速度时,或者它们足够复杂,需要控制其返回的内容。
测试 filter
为 filter 编写测试
- 过滤器是可以返回值的函数,因此你可以通过参数调用并断言它们返回了正确的值来测试。这非常简单!
测试组件中的过滤器
- 你不应该测试过滤器是否被显式使用。相反,你应该测试组件是否产生了正确的输出,以及其使用的过滤器是否产生了正确的输出。
编写快照测试
- 快照测试是一种自动比较应用程序的两张图片的方法。
- 快照测试第一次启动时,Jest 会用传递给 expect 的值来创建快照文件。下次运行快照测试时,它会将 expect 调用的新值与快照文件中保存的值进行比较。如果输出匹配,快照测试将通过。如果组件生成的 DOM 节点发生更改,那么新值与保存的值不同,快照测试将带着差异(diff)失败。
- Jest 可以为你管理快照文件。快照文件是以.snap 为扩展名,生成在snapshots中的文件,该目录与测试文件会在同一目录中被创建。快照文件是快照测试输出的唯一真实源,因此应该在源代码管理中包含快照文件,以便在不同设备上运行测试时使用。
为静态组件编写快照测试
- 静态组件(static component)指的是总是渲染相同输出的组件。它不接收任何 prop,也没有任何 state。组件中没有任何逻辑,并且总是会渲染相同的 HTML 元素。
- 你可以使用--update 标识调用 Jest,重写 snap 文件。
- 为静态组件编写快照测试非常有用,因为可以防止组件的渲染输出被意外更改。快照测试对于动态组件而言,将会更加实用。
为动态组件编写快照测试
- 动态组件(dynamic component)指的是那些包含逻辑和状态的组件。比如说,点击按钮时它们会传递 prop 或者更新数据。
- 当你为动态组件编写快照测试时,应该尝试捕获尽可能多的不同组合的状态。这样,快照测试将尽可能多地覆盖功能。
- 快照测试的一个准则是快照测试必须是可确定的。换句话说,如果生成输出的代码没有改变,那么输出应该总是相同的,不管启动多少次测试都应该是这样。
- 理想情况下,组件输出的所有分支都应该被快照测试覆盖到,但这并不总是可能的,也并不可取。一个组件的大量快照测试意味着每次更改该组件时,都会有大量失败的快照测试。如果有太多失败的快照测试,那么更新所有失败的测试可能会变得非常困难,并且可能会意外地保存一个错误的快照。一般来说,我不会为一个组件编写超过三个快照测试。
将快照测试添加到你的工作流
- 作者的工作流采用的是编写单元测试来覆盖核心组件功能。在进行单元测试之后,我不需要任何额外的测试就可以对组件的样式进行设置,并在浏览器中手动测试样式。当我对组件样式感到满意时,我就会为它添加一个快照测试。
- 因为要信任快照测试,所以将快照视为代码库的一部分是很重要的。当你审查包含新的或已更改的快照测试的拉取请求时,你应该像查看任何其他代码更改一样,仔细阅读快照代码。