- 开局掰扯
- 单元测试概念
- 单元测试意义
- 测试驱动开发
- 编码方式对比
- 在Vue中写测试
- 总结
- 参考文献
我们知道,在前端层面的单元测试是不受重视的,一方面对开发的要求高,另一方面业务代码的单元测试可能在一次需求变更后就需要重新写。
但是我们前端是有公共组件库和公共功能函数库,这俩者都对于稳定性有着较高的要求,十分适合单元测试的落地。
而单元测试可以很好的减少反复修改的问题,即修改了问题A打开了问题B,C的情况。
- 在计算机编程中, 单元测试 (英语:Unit Testing)又称为 模块测试 ,是针对程序模块)(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。
- 在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类(子类)中的方法。——维基百科
上面两个概念说的已经很详细了,技术开发过程中模块、功能、函数等最小单位的测试。每个测试应该是最简单的功能或函数。
减轻开发者负担
- 提前对需求有认知,先写单元测试(单元设计)可以帮助我们去思考需求,变相的等于去书写代码。
- 开发过程更流畅,只需要实现单元测试用例通过即可,流程更明确。
健壮的代码
- 测试的意义就是验证正确性,程序中的每项功能都是测试来验证他的正确性
- 思维的转变,编写测试用例使我们可以从用户或者是测试的角度观察思考。迫使我们把程序设计成易于调试和可测试的,即迫使我们解除程序中的耦合。
代码的履历
- 一个好的测试程序相当于书写了一个使用文档,使使用者即使不通过阅读文档也能够知道如何使用。
- 当需求变更后,即为变更测试用例,测试用例的变更记录方便开发能够回归查阅。
当我们了解了单元测试的概念和意义的时候,需要了解一些方法论。
这里我推荐TDD方法,也就是测试驱动开发。当然还有另外一些方法,比如BDD行为驱动开发。
他们俩个方法简单点来说,TDD也就是先写测试用例,再进行实际开发。BDD先进行实际开发,再对实际功能书写测试用例。
这里我主要来说TDD,TDD是测试驱动开发,侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug 更少的代码。
TDD的基本流程
")TDD的流程是个圈,无限循环。
第一步,我们实现一个测试用例,它是一个对最小单位的需求描述,只关心输入输出,不考虑内部如何实现。
第二步,我们实现功能,它是专注于快速实现测试用例的要求,不考虑其他需求,也不管代码质量如何。
第三步,我们对功能代码重构,它是既不用考虑需求,也不用考虑实现,而是找到代码中的坏味道,并设法消除他,让代码变得整洁。
之后,重复前三步,直到功能实现完美且通过测试。
下面的一张图可以清楚的明白TDD方式编写单元测试的流程:
在现在我们的编码过程中是需要不断地调试,不断的试错,并且不能保证代码时刻简洁。而”红-绿-蓝“ 这种方式是先用脏乱代码表达出来,测试通过后立刻重构代码,这是个不断循环的过程,
不能是写了很多实现代码后再开始重构,而是最小实现的时候 就开始重构代码,随时的重构代码,可以保证当你完成功能需求的时候,代码是时刻简洁可用的。
传统编码方式对比TDD编码方式
传统 | TDD |
---|---|
需求分析(写分析 | 分解任务-分离关注点 |
确认细节(详细设计 | 列例子-用实例化需求,澄清细节 |
开发(编码调试(编码过程调试BUG | 写测试-只关心输入输出,不考虑内部如何实现写实现-不考虑别的需求,用最简单的方式满足当前这个小需求即可写重构-用手法消除代码里的坏味道 |
加需求(重复开发与调试 | 重复写测试 写实现 写重构写完功能手动测试,有问题就补充测试用例,修复问题 |
QA测试 | QA测试-小问题,补用例,修复。 |
代码逻辑混乱,稍微改动会导致其他问题出现改好了后交付QA测试,有问题加班继续改 | 代码整洁且用例齐全,信心满满。 |
总结
不难看出,测试驱动开发的有点还是很明显,但具体还是要靠实施的人。就理论来讲,测试驱动开发最大的优点就是重构的步骤,不断地迭代,不断地对现有代码进行重构,不断地优化代码的内部结构,最终实现整体代码的改进,减少复杂度。
当然也有缺点,就是比较局限。它不能发现一些性能问题、或者其他系统级别的问题。还要求测试用例足够好足够准确,如果测试用例过于复杂那么可能这个测试用例本身就是错的,本身就可能有BUG。
目前来讲,在vue中对组件进行单元测试的方法很简单。
新建项目
我们只需要在创建项目的时候,将Unit Testing勾选上。如下图所示。
并在后续选项中选择你需要的测试套件,如下图所示。
已有项目
已有项目需要集成单元测试,需要手动安装依赖和创建文件,或者是使用Vue Cli的功能。
手动需要安装:@vue/cli-plugin-unit-jest 和 @vue/test-utils 两个依赖。再在根目录手动创建 “jest.config.js”文件和\tests\unit文件夹。内容如下:
|
// module.exports = {
// preset: '@vue/cli-plugin-unit-jest'
// }
module.exports = {
moduleFileExtensions: ['js', 'vue'],
transform: {
'^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
snapshotSerializers: ['jest-serializer-vue'],
// testMatch: ['tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
testMatch: ['**/tests/**/?(*.)+(test).[jt]s?(x)', '**/tests/**/*spec.[jt]s?(x)', '**/__tests__/**/*.spec.js'],
transformIgnorePatterns: ['<rootDir>/node_modules/'],
collectCoverage: true,
// collectCoverageFrom: ['tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', '!**/node_modules/**'],
}
Vue Cli Add: 在项目根目录,命令行输入 “vue add unit-jest” (注:此功能未经测试。
这里我选择了Jest,Jest有很多特点,具体我就不一一举例了,只需要知道它是目前比较新,但不影响使用。因为有Facebook背书。并且它支持较多的项目,如react,vue等都是可以支持。
我们通过Vue Cli创建的项目,打开后是如下结构的。
测试文件都存放在tests\unit目录下,当然这个目录是可以去进行更改的,只需要在jest.config.js中的testMatch更改匹配正则即可。
如何书写测试文件
首先举个例子,我们有一个组件库制作的需求,这个组件库有一个Button组件。
比如我们根据设计图得知,这个按钮可以创建四种不同类型的按钮和四种不同颜色的按钮,还可以进行禁用。
那么我们根据TDD方法,第一步我们先保证按钮能够渲染出来。想要实现一个判断按钮是否渲染的测试用例,应该先知道测试框架的语法。目前来讲,测试框架或多或少通用如下操作。
- describe: 描述你要测试的东西
- test: 对其进行测试
- expect: 判断是否符合预期
大致上测试框架都是如上三个操作。
举个例子,我要判断一个函数操作输入两个参数参数是数字,输出结果是否等于3。那么这个测试用例该怎么写呢?
|
const addSum = (a,b) => a+b
describe('测试一个功能函数',()=>{
it('函数返回值是否等于3',()=>{
expect(addSum(1,2)).toBe(3)
})
})
输出结果:
如果我们将函数调用改变呢?比如第二个参数该为3,会发生什么?输出结果:
它会告诉我们,哪个测试集中的哪个测试在哪一步出现了什么错误。比如这里,预期结果是3,结果得到4。那么自然而然我们这个测试就不会通过。
知道了如何书写一个最简单的函数的测试后,我们来试试如何书写对单文件组件的测试。新建一个文件,我们起名为Button.spec.js。
首先我们需要引入一个库,@vue/test-utils,它内部提供的函数可以运行你的单文件组件并返回单文件组件真正运行后的一些数据。其次引入你的单文件组件。
|
import { shallowMount } from '@vue/test-utils'
import Button from '@/components/button/Button.vue'
// 起一个测试集名为Button.vue
describe('Button.vue', () => {
let wrapper
// 测试 是否能够正常渲染button组件
it('是否能够正常渲染button组件', (done) => {
// 渲染单文件组件
wrapper = shallowMount(Button)
// 使用expect断言 wrapper.vm.$el.tagName是否等于BUTTON
// 断言函数看起来像expect(result).to [matcher] (actual)这样。matcher是用来比较值和对象的一些方法。
// 例如如下,Be或整体称为toBe,它是用来比较基本值或检查对象实例的引用标识。类似 ===
expect(wrapper.vm.$el.tagName).toBe('BUTTON')
done()
})})
我们初次执行测试后会发现测试失败了,是因为我们Button组件现在并没有内容。
那么我们遵循TDD原则,来实现一个最简单的实现。在Button组件内Template中插入
|
<template>
<button></button>
</template>
这样,我们以最小的代价实现了第一个测试用例的通过。
现在,我们来实现四种不同类型的按钮的测试用例,设想上,我们四种类型按钮区别只有样式不同,所以我们让我们的button根据条件切换样式,所以我们测试用例只需要传入当前类型,判断渲染后的button是否具有该有的样式。
在测试集中书写如下代码,执行测试
|
it('type属性是否生效', (done) => {
wrapper = shallowMount(Button, {
// 使用propsData传递props
propsData: {
type: 'success',
},
})
// 断言 wrapper.vm.$el内是否有tm-btn-success的class
expect(wrapper.vm.$el.classList.contains('tm-btn-success')).toBe(true)
wrapper = shallowMount(Button, {
propsData: {
type: 'error',
},
})
// 断言 wrapper.vm.$el内是否有tm-btn-error的class
expect(wrapper.vm.$el.classList.contains('tm-btn-error')).toBe(true)
})
书写功能实现,我们的按钮一共有 default', 'primary', 'dashed', 'text', 'gary', 'info', 'success', 'warning', 'error' 种类型。
|
<template> <button :class="classes"></button> </template><script>const prefixCls = 'tm-btn';
export default {
props: {
type: {
validator(value) {
return ['default', 'primary', 'dashed', 'text', 'gary', 'info', 'success', 'warning', 'error'].includes(value);
},
default: 'default'
}, cumputed: function () { classes() {
return [
`${prefixCls}`,
`${prefixCls}-${this.type}`,
];
}, }
}
}
</script>
执行测试
如此流程,循环往复,把整个Button组件制作出来,同时也将测试同步书写了出来,测试等同于文档,那么整个开发流程沉淀出来了三套产物(实际组件,测试文件,测试文件即文档)
总的来说呢,为代码书写测试,可以让我们的出现从开发一开始就保持严谨的姿态,即使进行到后期的维护时,我们也可以通过运行当初书写的测试来判断维护时修改的代码是否使其行为正确。
减少了开发人员出现常规错误的机会,使得代码更为健壮。当然也有人问,前端除了开篇提到的组件库和功能函数库需要写测试还有哪些需要写。
这里我可以给简单的看法,在我认为前端纯函数,业务, ui ,目前大家会对这三种进行测试,业务的逻辑代码最好与UI抽离,这样才可以进行单独测试。
如何把业务逻辑与UI抽离呢?简单来说就是不把功能函数写到组件中,而是在外部单独维护,组件只是把业务逻辑处理好的数据展现出来,没事时候调调业务方法。