初使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: ['<rootDir>/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'))	//true
    console.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)	// 3
    Wrapper.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);
  });

  // 创建select
  const getSelect = (params?:any)=>{
    return createVue({
      template: '<basic-select v-model="value" :options="options" v-bind="params" />',
      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为1
    selectDom.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: ['<rootDir>/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-post v-bind="post"></blog-post>

等价于:

<blog-post
  v-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’);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值