前言
前端工程越来越复杂,光靠工程师的常规工作来维护项目变得越发困难。在前端开发中引入自动化测试技术,让项目质量可以通过自动化工具来保障,将解决这个难题。从实际应用情况来说,大小公司也都越来越重视测试,大公司工程大,必然要测试;小公司分工没有那么细,要求”一角多能“,前端工程师更要承担测试工作。另外,掌握测试不只是一种技能,它更能提升你的架构思维、编码能力和把控项目整体稳定性的能力。
什么情况下要进行自动化测试
- 当项目比较重时:项目历史包袱比较重,难以维护,每次上线都提心吊胆
- 当开发基础库或工具时:当开发基础库或工具时,要确保代码质量,这个时候也要用到前端自动化测试
- 当引入新技术方案时:当你想引入新技术方案时,是用自动化测试可以避免很多”坑“,也适合学习之后在项目中使用
单元测试
又称模块测试,是针对软件设计的最小单位——程序模块进行正确性检验的测试工作。其目的在于检查每个程序单元能否正确实现详细设计说明中的模块功能、性能、接口和设计约束等要求,发现各模块内部可能存在的各种错误。单元测试需要从程序的内部结构出发设计测试用例。多个模块可以平行地独立进行单元测试
集成测试
也叫做组装测试。通常在单元测试的基础上,将所有的程序模块进行有序的、递增的测试。集成测试是检验程序单元或部件的接口关系,逐步集成为符合概要设计要求的程序部件或整个系统
单元测试选择工具
-
Jest 是功能最全的测试运行器。它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好。不过你需要一个能够将单文件组件导入到测试中的预处理器。我们已经创建了 vue-jest 预处理器来处理最常见的单文件组件特性,但仍不是 vue-loader 100% 的功能。jest 以 JS 开发,前端工程师可以零语言成本入门,是当前大多数工程师及公司的主流选择
-
mocha-webpack 是一个 webpack + Mocha 的包裹器,同时包含了更顺畅的接口和侦听模式。这些设置可以帮助我们通过 webpack + vue-loader 得到完整的单文件组件支持,但这本身是需要很多配置的。
学习过程
- jest 学习
- 实战项目:vue=>Vue Test Utils=>TDD 单元测试=>PDD 集成测试
- TDD 测试驱动开发(Test-Driven Development/red-green Development)
①开发流程
②优势- 长期减少回归 bug
- 代码质量更好(组织,可维护性)
- 测试覆盖率高
- 错误测试代码不容易出现
- PDD 页面驱动开发(Page-Driven Develop)
PDD就是一切以页面为核心,然后每个程序员针对每个页面来找到功能点,从而以页面为单位进行任务交付。PDD是先有功能,后拆分,是以以业务为驱动的。
在 vue 中实现单元测试
- Vue CLI 拥有开箱即用的通过 Jest 进行单元测试的内置选项。还有官方的 Vue Test Utils 提供更多详细的指引和自定义设置
- 已建项目配置 Jest 单元测试工具:vue-cli3 的插件使安装流程变得格外简单,通过 vue ui 启动可视化管理系统,在插件栏,点击 ‘添加插件’,搜索 @vue/cli-plugin-unit-jest,点击安装就可以了,对应命令行的 vue add @vue/cli-plugin-unit-jest 命令;这个过程实际上是包含了安装和调用两个步骤,并且会把相关的依赖一并安装进来,这样就不需要自己一个一个的安装每个依赖了。 1.已构建项目新增测试用例可能会遇到各种引用问题,遇到具体问题具体分析。或可以通过注释代码找到问题所在
- jest 会自动找到项目中所有使用.spec.js 或.test.js 文件命名的测试文件并执行,通常我们在编写测试文件时遵循的命名规范:测试文件的文件名 = 被测试模块名 + .test.js,例如被测试模块为 functions.js,那么对应的测试文件命名为 functions.test.js
jest.config.js 配置
单元测试例子
import { shallowMount, mount, createLocalVue } from '@vue/test-utils'
import store from '@/store/'
import ViewUI from 'view-design'
import * as filters from '@/libs/filter'
import config from '@/config'
import VueRouter from 'vue-router'
import PlacePage from '@/views/BaseInfo/PlacePage/index.vue'
import House from '@/views/BaseInfo/House/index.vue'
// import * as filters from "@/libs/filter";
// 相关api请求
const routes = [{ path: '/house', component: House }]
// 模拟router
const router = new VueRouter({
routes
})
const localVue = createLocalVue()
Object.keys(filters).forEach((key) => localVue.filter(key, filters[key]))
localVue.use(VueRouter)
localVue.config.productionTip = false
localVue.use(ViewUI, {
transfer: true,
size: 'default',
// capture: false,
select: {
arrow: 'md-arrow-dropdown',
arrowSize: 20
}
})
localVue.prototype.$config = config
describe('House.vue', () => {
router.push('/house')
const wrapper = mount(PlacePage, {
localVue,
router,
store
})
it('House.vue渲染正常', () => {
expect(wrapper.exists()).toBe(true)
})
})
vue-test-utils 是 vue 官方的单元测试框架,提供了一系列非常方便的工具,使我们更轻松地为 vue 构建的应用来编写单元测试。更详细内容请查看:Vue Test Utils 是 Vue.js 官方的单元测试实用工具库
jest是功能最全的测试运行器。它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好。不过你需要一个能够将单文件组件导入到测试中的预处理器。vue 已经创建了 vue-jest 预处理器来处理最常见的单文件组件特性,但仍不是 vue-loader 100% 的功能。
预研 jest 实现真实请求及 gitLab 环境实现请求
前端模拟返回数据自测
BaseInfo.House.getFloor = jest.fn()
BaseInfo.House.getFloor.mockResolvedValue(returnData(publicData.floorList))
设置超时时间及配置环境
解决断言默认 5000ms 超时问题,可在 test/it 中单独设置超时参数。也可在 jest.config.js 中已设置 60s 为默认超时时间,提出测试用例中公共部分-配置环境部分。配置于 setupFile 中。(setupFile:运行些代码以配置或设置测试环境的模块的路径列表。每个 setupFile 将对每个测试文件运行一次。由于每个测试都在其自己的环境中运行,因此这些脚本将在执行测试代码本身之前立即在测试环境中执行。另setupFiles 在环境中安装测试框架之前执行,在环境中安装测试框架后立即运行一些代码为setupFilesAfterEnv。) 更多请参考jest 配置项
module.exports = {
preset: '@vue/cli-plugin-unit-jest', // Jest配置基础的预设
globals: {
// 全局变量
GosunGIS: GosunGIS,
Cesium: Cesium
},
setupFiles: ['jest-canvas-mock', './tests/config/public.js'],
errorOnDeprecated: true,
testTimeout: 60000 // 超时默认为5000ms,此处设置为60s
}
axios 请求适配器
// 默认配合
var defaults = {
adapter: getDefaultAdapter()
....
}
// 分发函数
function getDefaultAdapter () {
var adapter;
if (typeof process !== 'undefined' && Object.prototype.toString.call(process)
=== '[object process]') {
// node环境下使用 node.http
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// web 环境下使用 xhr
adapter = require('./adapters/xhr'); // 基于XMLHttpRequest
}
return adapter;
// 使用 adapter 做mock mock数据路由,根据url 返回mock数据
const mockRouter = { ...}
const http = Axios.create({
adapter: config => {
// 判断是否存在mock数据
let has = mockRouter.has(config.url)
// 调用默认请求接口, 发送正常请求及返回
if (!data) {
// 删除配置中的 adapter, 使用默认值
delete config.adapter
// 通过配置发起请求
return Axios(config)
}
// 模拟服务,返回mock数据
return new Promise((res, rej) => {
const resInfo = {
data: mockRouter.getData(config)
status: 200,
statusText: 'OK'
}
// 调用响应函数
res(resInfo)
})
}
})
}
配置 baseURL
axios.defaults.baseURL = 'https://181.181.0.218'
配置环境-安装 fetch
- fetch 介绍 Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局 fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。 这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更理想的替代方案,可以很容易地被其他技术使用,例如 Service Workers。Fetch 还提供了专门的逻辑空间来定义其他与 HTTP 相关的概念,例如 CORS 和 HTTP 的扩展。 请注意,fetch 规范与 jQuery.ajax() 主要有三种方式的不同: 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject, 即使响应的HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返值的ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。 _ fetch() 可以不会接受跨域 cookies;你也可以不能使用 fetch() 建立起跨域会话。其他网站的 Set-Cookie 头部字段将会被无视。* fetch 不会发送 cookies。除非你使用了 credentials 的初始化选项。(自 2017 年 8 月 25 日后,默认的 credentials 政策变更为 same-origin。Firefox 也在 61.0b13 版本中进行了修改)
- fetch API
- 安装 fetch
$ npm install --save isomorphic-fetch
- 使用 fetch 先获取 token 等登录信息
import fetch from 'isomorphic-fetch'
const baseUrl = 'http://188.188.33.24:82'
const loginData = {
loginName: 'zidy001',
passWord: 'a123456'
}
// 使用fetch获取数据
const jestFetch = async function ({ url, data, params, headers, method }) {
// 请求接口前先确认token
if (!window.token) {
const { data: d } = await getToken()
window.token = d.token
}
return new Promise((resolve, reject) => {
params = params ? '?' + params : ''
const o = {
method: method
}
if (data) {
o.body = JSON.stringify(data)
}
if (headers) {
o.headers = headers
} else {
// 获取token
console.log('token', window.token)
o.headers = {
'access-token': window.token
}
}
console.log(params, o)
fetch(baseUrl + url + params, o)
.then((response) => {
return response.json()
})
.then((response) => {
resolve(response)
})
.catch((e) => reject(e))
})
}
const getToken = function () {
return new Promise((resolve, reject) => {
fetch(baseUrl + '/smart/smart-auth/user/login', {
body: JSON.stringify(loginData),
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
.then((response) => {
return response.json()
})
.then((response) => {
resolve(response)
})
.catch((e) => reject(e))
})
}
- 使用 fetch 发起真实请求,使用 async/await 异步获取真实返回值,可对返回值进行断言等操作
BaseInfo.House.getPlaceList = ({ placeType = '01' }) => {
return jestFetch({
url: '/smart/smart-base/house/define',
params: 'placeType=' + placeType,
method: 'GET'
})
} it('根据场所类型获取场所信息', async done => {
const ref = await BaseInfo.House.getPlaceList({ placeType: '' })
console.log(ref)
// { timestamp: 1611805779944,status: 0,message: '',data: [{ code:
'360102008001020001', name: '总部基地'
}, ...], total: 9, row: 9}
expect(ref).toBeTruthy()
done()
})
运行单元测试
- package.json 添加执行命令
"scripts": {
"test:unit": "vue-cli-service test:unit",
"coverage": "jest --coverage",
// "test": "jest"
}
- 查看测试覆盖率
npx jest --coverage
默认生成 coverage 文件夹 打开里面 lcov-report-index,查看测试覆盖率 测试报告分四个部分:80%以下不及格,90%以上可以用,95 以上优秀。
- statement:语句覆盖率,顾名思义,有多少语句被测到。
- branches:分支覆盖率,有多少逻辑分支被测到。
- function:函数覆盖率,有多少个函数被测到。
- lines:线路覆盖率,有多少个逻辑线路被测到