这篇文章是Vue组件单元测试的指南。
我们首先会看看为什么单元测试对创建可维护的软件很重要,以及你应该测试什么。然后,我们将详细介绍如何。
- 为Vue组件创建并运行单元测试
- 测试Vue组件的不同方面
- 使用mock来测试异步函数
- 检查单元测试的代码覆盖率
- 构造你的单元测试文件
如果你有兴趣学习更多关于Vue的知识,请查看我的课程。通过构建和部署一个CRUD应用程序来学习Vue。
依赖性。
本文中使用的Vue天气应用程序项目的源代码(以及详细的安装说明)可以在GitLab上找到。Vue Weather App。
目标
在本文结束时,你应该能够。
- 解释为什么单元测试很重要
- 描述你应该(和不应该)进行单元测试的内容
- 为Vue组件开发一个单元测试套件
- 使用Vue CLI为一个Vue项目运行单元测试
- 在单元测试套件中使用
beforeEach()和afterEach()功能 - 编写单元测试来测试Vue组件的实现细节
- 编写单元测试来测试Vue组件的行为方面(点击事件等)。
- 解释嘲弄是如何帮助简化单元测试的
- 编写单元测试来模拟库和测试异步函数
- 检查你的单元测试的代码覆盖率
- 开发一个结构良好的单元测试文件来测试Vue组件
为什么要进行单元测试?
一般来说,测试有助于确保你的应用程序将为你的终端用户提供预期的工作。
具有高测试覆盖率的软件项目永远不会是完美的,但它是软件质量的一个良好的初始指标。此外,可测试的代码通常是一个良好的软件架构的标志,这就是为什么先进的开发人员在整个开发生命周期中都会考虑到测试。
测试可以从三个层面考虑。
- 单元
- 集成测试
- 端到端
单元测试是测试独立的代码单元的功能,与它的依赖关系隔离。它们是防止代码库中错误和不一致的第一道防线。单元测试是测试驱动开发(TDD)过程的一个基本部分。
单元测试提高了你的代码的可维护性。
可维护性是指对你的代码进行错误修复或增强,或在未来的某个时间点,另一个开发人员需要更新你的代码。
单元测试应该与持续集成(CI)过程相结合,以确保你的单元测试不断地执行,最好是在你的存储库的每次提交中。一套扎实的单元测试对于在开发过程中快速捕捉缺陷是至关重要的,在终端用户在生产中遇到这些缺陷之前就已经开始了。
测试什么
你应该测试什么?或者,更重要的是。你不应该测试什么?
在单元测试中,有三种类型的测试需要考虑。
- 实施细节:一个组件用来产生基于给定输入的结果的底层业务逻辑。
- 公共接口/设计合同:特定的输入(或道具,在这种情况下)产生特定的结果。
- 副作用。"如果这样,那么那样";即,当一个按钮被点击时,会发生一些事情。
既然你不能测试所有的东西,你应该关注什么?
专注于测试终端用户将与之互动的输入和输出。你的产品的用户的体验是最重要的!
- 输入:数据、道具、用户交互、生命周期方法、Vuex存储、路由参数、查询字符串
- 输出:渲染的输出、事件、数据结果、Vuex商店的更新、调度
通过专注于测试软件模块(即Vue组件)的输入/输出,你正在测试终端用户将体验的关键方面。
在软件模块中可能还有其他复杂的内部逻辑,需要进行单元测试,但这些类型的测试最有可能在代码重构期间需要更新。
应用程序概述
在讨论如何对Vue组件进行单元测试之前,我想对我们要测试的Vue天气应用程序做一个简短的概述。
在克隆完repo后,安装依赖项,并添加API密钥。
查看项目的README,了解更多关于创建和添加Open Weather的API密钥的信息。
一旦完成,Vue天气应用程序可以通过启动开发服务器在你的本地计算机上运行。
一旦应用程序建立起来,你会看到一个类似的成功信息。
在这一点上,开发服务器将启动并运行。你可以通过在你喜欢的网络浏览器中导航到http://localhost:8080,看到Vue应用程序。当你第一次加载应用程序时,没有显示任何数据;你只看到一个输入字段,输入你想获得的天气的城市。
如果没有输入任何数据,"搜索 "和 "清除 "按钮都是无效的。
一旦你开始输入一个城市的数据(只要第一个字符被添加),"搜索 "和 "清除 "按钮都将被启用。
如果您在输入一个有效的城市后点击'搜索',该城市的天气数据将被显示。
在这一点上,点击 "清除天气数据 "按钮将导致天气数据被清除。
然而,输入的城市将保留在输入栏中。
如果您现在点击 "清除",输入栏将被清除,"搜索 "和 "清除 "按钮将再次失效。
如果你输入了一个无效的城市会怎样?如果你在'清除天气数据'之前点击'清除'会怎样?你能发现应用程序的其他什么状态?
由于组件是Vue(实际上是任何SPA框架)的构建块,它们是你的应用程序整体中最关键的部分。因此,把你的大部分测试时间用于编写单元测试,测试你的应用程序的组件。
根据我对Vue组件进行单元测试的经验,我发现测试工具和框架--Vue Test Utils和Jest--满足了良好测试环境的关键方面。
- 测试很容易写,也很有趣
- 测试可以快速编写
- 测试可以通过一个命令来执行
- 测试运行迅速
希望你会发现,单元测试你的Vue组件是一种愉快的体验,因为我认为这是鼓励更多测试的关键。
单元测试工具
有两个工具,我们将用来对Vue组件进行单元测试。
- Vue Test Utils- Vue的官方单元测试工具库
- Jest- 用于查找和执行单元测试的测试运行器
单元测试概述
首先,让我们讨论一下Vue中单元测试的命名规则。单元测试文件应采用以下格式。
<Component Name>.spec.js
通常情况下,你应该为Vue项目中的每个组件都有一个单元测试文件。在每个单元测试文件中,可以有一个单元测试套件或多个单元测试套件。
单元测试文件应该与Vue组件分开放置,比如放在 "test/unit "文件夹中。
运行测试
Vue CLI可以通过执行来使用Jest运行单元测试。
例子
使用Vue天气应用程序,让我们看看一些测试Vue组件的例子。
例子1 - 单元测试介绍
让我们直接进入Vue的单元测试文件的例子吧!第一个单元测试文件在test/unit/Header.spec.js中找到,它测试的是Header 组件。
安装
这个文件的第一行从Vue Test Utils库中导入了一个名为shallowMount 的函数。安装 "的概念是指加载一个单独的组件,以便能够测试它。在Vue Test Utils中有两种方法。
shallowMount()- 为Vue组件创建一个 ,但有存根的子组件wrappermount()- 为Vue组件创建一个 ,包括安装任何子组件。wrapper
由于我们的重点是测试一个单独的组件(Header 组件),我们将使用shallowMount() 。
shallowMount()是更好的测试单独的组件,因为子组件被存根了。这是单元测试的理想情况。此外,使用
shallowMount()来测试一个有很多子组件的组件,可以提高单元测试的执行时间,因为渲染子组件没有成本(在时间上)。
mount()当你想包括测试子组件的行为时,使用 是很有用的。
第二行导入要测试的Vue组件,Header.vue。
描述区块
在import 语句之后,有一个describe 块,定义了一个单元测试套件。
在一个单元测试文件中,可以有多个describe 块来定义不同的单元测试套件。同样,每个describe 块可以包含多个单元测试,其中每个单元测试由一个it 块定义。
我认为这种区别为。
describe块--单元测试套件it块--单独的单元测试功能
在Vue中,单元测试真正好的地方在于,有许多内置的鼓励措施来添加注释。例如,describe 语法要求第一个参数是测试套件的名称。当测试Vue组件时,在这里包括Vue组件的名称是最简单的。
对于每个it 块,第一个参数是测试功能的描述,应该是对这个具体测试的简短描述。在上面的例子中,it 块测试该组件 "在组件创建时渲染消息"。
期望
就实际的单元测试而言,第一步是装载Vue组件,以便对其进行测试。
shallowMount 函数返回一个wrapper 对象,其中包含了安装的组件和测试该组件的方法。wrapper 对象允许我们测试由Vue组件生成的HTML的所有方面,以及Vue组件的所有属性(如数据)。
此外,传入Header 组件的道具,被作为第二个参数传入shallowMount() 。
在单元测试中实际执行的检查是。
这些行在wrapper ,以测试Vue组件,执行以下检查。
- 检查该组件的名称是否为
Header - 检查该组件生成的标题是否为 "Vue项目"。
这两个检查都是比较字符串,所以推荐使用的测试函数是toMatch() 。
Jest帮助器
虽然Header 组件的单元测试文件中的检查只是检查字符串值,但Jest有很多选项可用于执行检查。
- 布尔运算。
toBeTruthy()- 检查一个变量/语句是否为真toBeFalsy()- 检查变量/语句是否为假
- 定义了。
toBeNull()- 检查一个变量是否只匹配nulltoBeUndefined()- 检查一个变量是否未被定义toBeDefined()- 检查一个变量是否被定义
- 数字。
toBeGreaterThan()- 检查一个数字是否大于指定的值toBeGreaterThanOrEqual()- 检查一个数字是否大于或等于指定的值toBeLessThan()- 检查一个数字是否小于指定的值toBeLessThanOrEqual()- 检查一个数字是否小于或等于指定的值toBe()和 - 检查一个数字是否与指定的值相同(这些函数对数字来说是等价的)。toEqual()toBeCloseTo()- 检查一个数字是否在一个小的公差范围内等于指定的值(对浮点数字很有用)
- 字符串。
toMatch()- 检查一个字符串是否等于指定的值(可以使用Regex作为指定的值!)。
- 数组。
toContain()- 检查一个数组是否包含指定的值
此外,not 修饰符可以用于大多数这些检查。
例2 - 测试初始条件
这个例子展示了如何测试VueWeather 组件的初始条件(或状态)。
下面是单元测试文件的概要(定义在test/unit/Weather.spec.js)。
该单元测试文件利用shallowMount() 函数来渲染Weather 组件,因为这个组件是作为一个独立的组件来测试的。
BeforEach 和 AfterEach 块
在单元测试套件内(在describe 块内定义),有两个新的函数被定义。
beforeEach()-在该单元测试套件内的每个单元测试执行前调用afterEach()- 在该单元测试套件内的每个单元测试执行完毕后调用。
beforeEach() 函数是用来在运行每个单元测试之前设置一个一致的状态。这个概念非常重要,可以确保单元测试的运行顺序不会影响整个单元测试的结果。
在这个例子中,beforeEach() 函数用一组默认的道具数据渲染了组件。
afterEach() 函数被用来清理单元测试期间进行的任何处理。
在这个例子中,afterEach() 函数销毁了单元测试期间使用的wrapper ,这样就可以在beforeEach() 中为下一个单元测试重新初始化wrapper 。
如果你想运行在整个单元测试套件运行之前或之后执行的代码,你可以使用。
预期
第一个单元测试检查Weather 组件的初始条件。
检查。
- 第一个期望检查该组件的名称是否与
Weather组件中定义的名称Weather相匹配。 - 第二部分的期望检查两个标题(定义为
h2元素)是否符合期望。 - 第三部分的期望检查六个数据字段(定义为
p元素)是否符合期望。
例3--测试道具
第二个单元测试检查作为道具数据传入的有效数据是否被Weather 组件正确处理。
由于beforeEach() 函数提供了一组默认的道具数据,我们需要使用setProps() 函数来覆盖道具数据。
检查。
- 随着道具数据的更新,我们可以通过检查数据元素(使用
wrapper.vm)来检查道具数据是否被正确地存储在Weather组件中。 - 第二部分的期望值检查两个标题(定义为
h2元素)是否符合预期。 - 最后一组期望检查道具数据用于设置六个数据字段(定义为
p元素)是否符合期望。
例四--测试用户输入(点击事件)
第三个单元测试检查当用户点击 "清除天气数据 "按钮时,clear-weather-data 事件是否由Weather 组件发出来。
为了触发点击事件,必须在wrapper 中找到button 元素,然后调用trigger 函数来触发点击事件。
一旦按钮被点击,单元测试检查只有一个自定义事件(名称为clear-weather-data )被发射出来。
嘲弄实例
在App 组件中,当用户搜索一个城市的天气时,一个HTTP GET调用到Open Weather,通过一个叫Axios的第三方库检索数据。
当考虑到如何测试HTTP GET调用时,有两种情况出现在脑海中,每种情况都是测试实际API调用的副作用。
- HTTP GET响应成功(快乐路径)
- HTTP GET响应失败(异常路径)
当测试利用外部API的代码时,用一个模拟的调用来代替实际的调用往往更容易。不过,这种方法有优点和缺点。
优点。
- 测试不会依赖于网络请求
- 如果API发生故障,它们不会中断
- 运行速度会快很多
缺点。
- 每当API模式发生变化时,你都需要更新测试。
- 在微服务架构中很难跟上API的变化
- 嘲弄是一个很难掌握的概念,它们会给你的测试套件增加很多杂乱的东西。
在某些时候,你应该检查完全集成,以确保API响应的形状没有改变。
由于本文的重点是单元测试,我们将对Axios库进行模拟。
嘲弄提供了一种模仿软件模块的预期行为的方法。虽然嘲弄可以用于生产代码(非常危险!),但它通常在开发和测试期间使用。
从外部API加载数据需要时间。虽然在这个应用程序中,从Open Weather加载数据通常需要不到一两秒的时间,但其他外部API可能会更耗时。此外,我们希望有一种方法可以轻松检查HTTP GET请求是否失败。因此,我们将添加mocks来指定GET请求的响应方式。
例5 - 测试异步代码(成功案例)
App 组件的单元测试位于test/unit/App.spec.js文件中。
为了开始测试,我们需要import Axios库。
然而,我们不想像源代码*(App.vue*)中那样使用实际的Axios库。相反,我们要创建一个Axios库的模拟,这样我们就不会实际调用外部API。
这一行告诉Jest(负责执行单元测试的框架),我们要模拟Axios库。
在单元测试文件*(test/unit/App.spec.js*)中,对于App 组件,我们要。
- 模拟一个成功的HTTP GET请求
- 模拟一个失败的HTTP GET请求
首先,让我们来处理HTTP GET请求成功的名义情况。
BeforeEach和AfterEach区块
在beforeEach() 函数中,我们设置了当axios.get() 被调用时应该出现的响应。响应是预先计划好的天气数据,看起来类似于我们从Open Weather得到的数据,如果我们真的提出请求的话。本节中的关键行是。
这一行是关键,因为它让Jest知道,当axios.get() 在App 组件中被调用时,它应该返回responseGet 数组,而不是使用Axios实际进行HTTP GET调用。
Jest提供的
mockResolvedValue()函数实际上是一种速记语法,用于。
jest.fn().mockImplementation(() => Promise.resolve(value));
单元测试套件的下一节定义了afterEach() 函数。
afterEach() 函数,在每个单元测试执行后被调用,清除在单元测试执行过程中创建的任何mock。这种方法是一个很好的做法,在运行测试后清理任何模拟,以便任何后续的测试从一个已知的状态开始。
期待
现在我们可以定义单元测试了。
由于我们已经经历了定义模拟和渲染组件的步骤(通过shallowMount() ),这个单元测试可以专注于执行检查。
该单元测试从调用searchCity() 函数开始。
我们检查axios.get() 只被调用了一次,并且HTTP GET调用包括正确的城市名称。
为了非常彻底,天气数据还检查本单元测试中渲染的App 组件的实例,以确保它与从axios.get() 的模拟中返回的预制数据相匹配。
什么是Vue.nextTick()?
上面这组检查并不那么直接,因为它们需要调用
nextTick()函数。我们用于测试Vue组件的单元测试框架是Vue Test Utils。这个框架做了一个简化,通过同步应用DOM更新来支持更容易的测试。
然而,当使用Axios库的外部API工作时,我们实际上是在处理异步行为(回调/承诺)。
为了确保DOM在HTTP GET调用成功的回调之后被更新,我们需要利用
nextTick()函数来推进事件循环,以便我们的HTTP POST调用的 "成功 "块中的更新被应用到DOM中。如果你改变上面的代码,不利用
nextTick(),你会发现这些检查会失败。
例6--测试异步代码(失败案例)
当事情按预期进行时,测试是很好的,但检查我们的软件对负面情况的反应也很重要。考虑到这一点,让我们创建第二个单元测试套件来检查一个失败的HTTP GET请求。
在一个单元测试文件(.spec.js)中拥有多个单元测试套件是没有问题的。
由于mocks是在
beforeEach()函数中创建的,所以需要有单独的单元测试套件,对成功和失败的HTTP GET响应进行不同的beforeEach()实现。
BeforeEach和AfterEach区块
这个单元测试套件中的beforeEach() 函数是非常不同的。
我们现在不是设置一个响应来返回axios.get() ,而是返回一个失败的Promise 对象,响应为'BAD REQUEST'。
这个单元测试套件的afterEach() 函数与其他单元测试套件相同。
期望
这里是测试一个失败的HTTP GET请求的单元测试函数。
就像之前的单元测试套件一样,我们检查只有一个axios.get() 的实例被调用,然后有一个检查,即没有天气数据被加载到App 组件中。
代码覆盖
在开发单元测试时,了解有多少源代码被实际测试是很好的。这个概念被称为代码覆盖率。
我需要非常清楚的是,拥有一套覆盖100%源代码的单元测试,决不是表明代码被正确测试了。
这个指标意味着有大量的单元测试,并且在开发单元测试方面投入了大量的精力。单元测试的质量仍然需要通过代码检查来检验。
另一个极端是,这是一套最小的(或没有!)单元测试,这是一个非常糟糕的指标。
Jest(单元测试运行器)的配置可以在jest.config.js中找到。
根据你在最初创建Vue应用时的配置方式,Jest的配置可以在jest.config.js中("在专用配置文件中 "是所选的配置选项)或在package.json中("在package.json中 "是所选的配置选项)。
为了配置Jest以生成我们单元测试的代码覆盖率,我们需要在该文件中添加以下配置项。
这些配置项告诉Jest,我们希望生成代码覆盖率(collectCoverage ),从哪里生成覆盖率数据(collectCoverageFrom ),以及如何报告输出(coverageReporters )。
有了Jest的这些额外配置,让我们在Vue CLI中重新运行单元测试。
单元测试结构
在经历了许多不同的单元测试文件后,我推荐Vue组件的单元测试文件采用以下结构。
关键项目。
- 每个单元测试套件都有一组相关的单元测试
- 利用
beforeEach()和afterEach()函数来创建独立的单元测试函数 - 为被测试的Vue组件内使用的任何库创建模拟。
- 在
beforeEach()函数中渲染组件,在单元测试函数中更新道具数据 - 利用
shallowMount(),而不是mount(),以专注于测试单个组件。
总结
本文提供了一个Vue组件单元测试的指南,重点是。
- 为什么要写单元测试
- 你应该(或不应该)进行单元测试的内容
- 如何编写单元测试
简单地说,当考虑测试什么时,重点是测试输入和输出(实际结果),而不是底层业务逻辑(结果是如何产生的)。考虑到这一点,花一两分钟再次回顾这些例子,注意测试的输入和输出。
同样,如果你有兴趣学习更多关于Vue的知识,请查看我的课程。通过构建和部署CRUD应用程序学习Vue。