初使jest 单元测试

前言

最近业务需求不多,所以开始整项目的测试。首先申明项目vue2.0 + ts构建的。
自动化测试
关于自动化测试的定义相关的知识,可以翻阅其他文章。这里只简单的做一下介绍哦。

前端测试主要分为 3 种:单元测试(Unit Test)、集成测试(Integration Test)、UI 测试(UI Test),同时我看到也有很多文章提到了End-to-end(端到端测试)

事实上,UI 测试(UI Test)和端到端测试(E2E Test)是稍有区别的:

UI 测试(UI Test)只是对于前端的测试,是脱离真实后端环境的,仅仅只是将前端放在真实环境中运行,而后端和数据都应该使用 Mock 的。
端到端测试(E2E Test)则是将整个应用放到真实的环境中运行,包括数据在内也是需要使用真实的。
就前端而言,UI 测试(UI Test)更贴近于我们的开发流程。在前后端分离的开发模式中,前端开发通常会使用到 Mock 的服务器和数据。因而我们需要在开发基本完成后进行相应的 UI 测试(UI Test)。

单元测试: 单元测试是测试一个模块,不依赖任何外部资源
集成测试: 测试一个模块或者多个模块,并伴随着它们对应的外部依赖资源,它测试的是应用代码的集成性,比如文件或者数据库。

测试框架

目前市面上比较流行的前端测试框架有Mocha、QUnit、Jasmine、Jest等,以下作个简单介绍
请添加图片描述

jest基本使用

安装

如果是后期添加单元测试的话,首先要安装Jest和Vue Test Utils:

vue add @vue/cli-plugin-unit-jest

这个命令会帮我们把相关的配置都配好,相关的依赖都装好,还会帮我们生成一个jest.config.js文件。

jest中常用的一些配置项

module.exports = {preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',testMatch: ['**/tests/spec/**/**/*.spec.[jt]s?(x)', '**/__tests__/*.[jt]s?(x)'],setupFiles: ['/tests/setup']
};

(3)在项目目录中创建tests文件,再创建xxxx文件,在其中文件命名的话,就以 xxx.spec.js命名

(4)在package.json 中添加启动命令,然后通过在控制台执行npm run test:unit ,进行测试

Scripts配置:

 "test:unit": "vue-cli-service test:unit"

基本语法规则

(1)Jest 支持三种方式写测试代码

  • .spec.js
  • .test.js
  • 放在 tests文件夹下

(2)测试结构

describe: 将几个相关的测试放到一个组中,非必须
test :(别名it)测试用例,是测试的最小单位
expect:提供很多的matcher 来判定你的方法返回值是否符合特定条件

describe('add的方法测试',()=>{test('2+3应该等于5',()=>{expect(add(2,3)).toBe(5)})
})

(3)mock方法和 处理

Jest的mock方式 (Jest.fn()、Jest.spyOn()、Jest.mock())
预处理和后处理
beforeAll / afterAll : 对测试文件中所有的用例开始前/ 后 进行统一的预处理
beforeEach/ afterEach : 在每个用例开始前 / 后 进行预处理
(4)覆盖率指标

在package.json中 设置 --coverage 即可 测试覆盖率

 "test:unit": "vue-cli-service test:unit --coverage"

%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
%Lines行覆盖率(line coverage):是不是每一行都执行了?

配置好后,会生成一个coverage文件,然后打开里面的index.html 里面会有详细的信息展示
三种颜色分别代表不同比例的覆盖率(<50%红色,50%~80%灰色, ≥80%绿色)
旁边显示的1x代表执行的次数
80%以下不及格,90%以上可以使用,95%以上优秀

jest基本语法

4.常用的方法
–mount: 创建一个包含被挂载和渲染的 Vue 组件的 wrapper,它仅仅挂载当前实例
—shallowMount:和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,只挂载一个组件而不渲染其子组件 (即保留它们的存根),这个方法可以保证你关心的组件在渲染时没有同时将其子组件渲染,避免了子组件可能带来的副作用(比如Http请求等)
—shallowMount和mount的区别:在文档中描述为"不同的是被存根的子组件",大白话就是shallowMount不会加载子组件,不会被子组件的行为属性影响该组件。

为什么使用shallowMount而不使用mount?

    ---我认为单元测试的重点在"单元"二字,而不是"测试",想测试子组件再为子组件写对应的测试代码即可

—Wrapper:常见的有一下几种方法

Wrapper:Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
Wrapper.vm:这是该 Vue 实例。你可以通过 wrapper.vm 访问一个实例所有的方法和属性。
Wrapper.classes: 返回是否拥有该class的dom或者类名数组。
Wrapper.find:返回第一个满足条件的dom。
Wrapper.findAll:返回所有满足条件的dom。
Wrapper.html:返回html字符串。
Wrapper.text:返回内容字符串。
Wrapper.setData:设置该组件的初始data数据。
Wrapper.setProps:设置该组件的初始props数据。 (这是使用了,但没有效果)
Wrapper.trigger:用来触发事件。

<template><div class="jest"><div class="name">{{name}}</div><div class="name">{{name}}{{text}}</div><div class="text" @click="add">{{text}}</div></div>
</template>
<script src="./script.js">
export default {name:"Foo",props:{name:{type: String,default: '啦啦啦'}},data() {return {text: 123}},methods:{add(){this.text += 1}}
}
</script>开始测试import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'describe('Foo', () => {const wrapper = shallowMount(Foo)console.log(Wrapper.classes())	//['jest']console.log(Wrapper.classes('jest'))	//trueconsole.log(Wrapper.find('.name').text())	// 切记如果是类的话,要加点  : 啦啦console.log(Wrapper.findAll('.name'))	//返回dom数组  WrapperArray { selector: '.name' }console.log(Wrapper.findAll('.name').at(0))	//取dom数组中的第一个Wrapper.setData({text : 3})   //  设置一个值 console.log(Wrapper.vm.text)	// 3Wrapper.setProps({name : "拉拉"})console.log(Wrapper.vm.name)	//这个结果仍 为 啦啦啦Wrapper.find('.text').trigger("click")console.log(Wrapper.vm.text) // 4
})
 也可以初始化某些数据
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'const wrapper = shallowMount(Foo,{data() {return {bar: 'lala'}},propsData:{message: 'dd'},mocks: {$route: {query: {aaa: '1',}},$router: {push: jest.fn(),replace: jest.fn(),}}})

5 Jest-Api(使用不同匹配器可以测试输入输出的值是否符合预期)
toBe:判断是否相等
toBeNull:判断是否为null
toBeUndefined:判断是否为undefined
toBeDefined:与上相反
toBeNaN:判断是否为NaN
toBeTruthy:判断是否为true
toBeFalsy:判断是否为false
toContain:数组用,检测是否包含
toHaveLength:数组用,检测数组长度
toEqual:对象用,检测是否相等
toThrow:异常匹配

describe('Foo', () => {expect(2 + 2).toBe(4)expect(null).toBeNull()expect(undefined).toBeUndefined()let a = 1;expect(a).toBeDefined()a = 'ada';expect(a).toBeNaN()a = true;expect(a).toBeTruthy()a = false;expect(a).toBeFalsy()a  = [1,2,3];expect(a).toContain(2)expect(a).toHaveLength(3)a = {a:1};expect(a).toEqual({a:1})
})

模拟一个真实使用

import BasicSelect from '@/components/form/BasicSelect.vue';
import { OptionModel } from '@/components/form/FormBase';
import { createVue, destroyVM } from '../../../utils'const options:OptionModel[] = [{ label: 1, value: 1, disabled: false },{ label: 2, value: 2, disabled: false },{ label: 3, value: 3, disabled: false },{ label: 4, value: 4, disabled: false }
];const value = ''describe('BasicSelect', () => {// 销毁let select;afterEach(() => {destroyVM(select.vm);});// 创建selectconst getSelect = (params?:any)=>{return createVue({template: '',data() {return { value,options,params};},components: { BasicSelect }});}it('select选中后值变更', (done) => {select = getSelect();expect(select.vm.value).toBe(''); // 未选之前 value为空options.forEach((item:OptionModel,index:number)=>{(select.vm.$el.querySelector('.el-select') as HTMLElement)?.click();(select.findAll('.el-select-dropdown__item')).at(index).trigger('click')expect((select.vm as any).value).toEqual(item.value);})done();})it.only('disabled select--------下拉框禁止点击', (done) => {select = getSelect({ disabled: true });expect(select.find('.el-input').classes()).toContain('is-disabled')// expect(select.vm.$el.querySelector('.el-input').classList.contains('is-disabled')).toBeTruthy();done();})it('disabled option--------列表第一个禁止点击disabled', done => {select = getSelect();select.vm.options[1].disabled = true;setTimeout(() => {// 判断第一个是不是包含is-disabled类const options = select.vm.$el.querySelectorAll('.el-select-dropdown__item');expect(options[1].classList.contains('is-disabled')).toBeTruthy();options[1].click();setTimeout(() => {expect(select.vm.value).toBe('');done();}, 100);}, 100);});it('clearable--------清空选择项', done => {select = getSelect({clearable: true});const selectDom= select.vm.$children[0].$children[0];select.vm.value = 1; // 设置value为1selectDom.inputHovering = true;setTimeout(() => {const iconClear = select.find('.el-input__icon.el-icon-circle-close')expect(iconClear.exists()).toBeTruthy();iconClear.trigger('click')expect(select.vm.value).toBe('');done();}, 100);});
})

题外话

  1. 引入了Element ui ,Element 组件会报错,提示没有注册,比如HelloWorld 组件中使用到了el-button组件,就会报错。 解决的话,还是创建一个vue的临时实例,将其挂载上去
  • 可以使用createLocalVue
import { config,createLocalVue } from '@vue/test-utils';
import ElementUI from 'element-ui';
const testVue = createLocalVue();
testVue.use(ElementUI);
export const localVue = testVue;
  • 也可以使用stubs解决 但是这个方式 不太合适 是一个数组 里面存放使用的组件
mount(Compo, { stubs: ['el-button] });
  • 在tests文件下下创建setup.js
import Vue from 'vue';
import ElementUI from 'element-ui';
Vue.use(ElementUI);

然后再jest.config.js下 setupFiles: [‘/tests/setup’]

module.exports = {...setupFiles: ['/tests/setup']
};
  1. vue–prop
    传入一个对象的所有 property
    如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。例如,对于一个给定的对象 post:
post: {id: 1,title: 'My Journey with Vue'
}

下面的模板:```typescript

等价于:

<blog-postv-bind:id="post.id"v-bind:title="post.title"
></blog-post>

参考文章
详解Jest结合Vue-test-utils使用的初步实践
关于Vue中用jest测试
Jest 从入门到入坟
一文搞定前端自动化测试(基础篇)
jest官方文档
断言归纳

第一次使用jest 欢迎大家指导~

一些常用的方法
键盘: xx.trigger(‘keyup.ctrl.enter’);


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部