🏂 写在后面
对于前端单元测试,其实两年前我就曾经关注了,但那时候只是简略的晓得 断言
,想着也不是太难的货色,我的项目中也没有用到,而后就想当然的认为本人就会了。
两年后的明天,部门要对以往的我的项目补加单元测试。真到了开始着手的时候,却懵了 😂
我认为的我认为却把本人给坑了,我发现自己对于前端单元测试无所不知。而后我翻阅了大量的文档,发现基于 dva
的单元测试文档比拟少,因而在有了一番实际之后,我梳理了几篇文章,心愿对于想 应用 Jest 进行 React + Dva + Antd 单元测试
的你能有所帮忙。文章内容力求深入浅出,浅显易懂~
介于内容全副收在一篇会太长,打算分为两篇,本篇是第一篇,次要介绍如何疾速上手
jest
以及在实战中罕用的性能及api
🏈 前端自动化测试产生的背景
在开始介绍 jest
之前,我想有必要简略论述一下对于前端单元测试的一些根底信息。
- 为什么要进行测试?
在 2021 年的明天,构建一个简单的
web
利用对于咱们来说,并非什么难事。因为有足够多优良的的前端框架(比方React
,Vue
);以及一些易用且弱小的UI
库(比方Ant Design
,Element UI
)为咱们保驾护航,极大地缩短了利用构建的周期。然而疾速迭代的过程中却产生了大量的问题:代码品质(可读性差、可维护性低、可扩展性低)低,频繁的产品需要变动(代码变动影响范畴不可控)等。因而单元测试的概念在前端畛域应运而生,通过编写单元测试能够确保失去预期的后果,进步代码的可读性,如果依赖的组件有批改,受影响的组件也能在测试中及时发现谬误。
-
测试类型又有哪些呢?
个别常见的有以下四种:
- 单元测试
- 功能测试
- 集成测试
- 冒烟测试
-
常见的开发模式呢?
TDD
: 测试驱动开发BDD
: 行为驱动测试
🎮 技术计划
针对我的项目自身应用的是 React + Dva + Antd
的技术栈,单元测试咱们用的是 Jest + Enzyme
联合的形式。
Jest
对于 Jest
,咱们参考一下其 Jest 官网,它是Facebook
开源的一个前端测试框架,次要用于 React
和React Native
的单元测试,已被集成在 create-react-app
中。Jest
特点:
- 零配置
- 快照
- 隔离
- 优良的 api
- 疾速且平安
- 代码覆盖率
- 轻松模仿
- 优良的报错信息
Enzyme
Enzyme
是 Airbnb
开源的 React
测试工具库,提供了一套简洁弱小的 API
,并内置Cheerio
,同时实现了jQuery
格调的形式进行 DOM
解决,开发体验非常敌对。在开源社区有超高人气,同时也取得了 React
官网的举荐。
📌 Jest
本篇文章咱们着重来介绍一下 Jest
,也是咱们整个React 单元测试
的根基。
环境搭建
装置
装置 Jest
、Enzyme
。如果React
的版本是 15
或者 16
,须要装置对应的enzyme-adapter-react-15
和enzyme-adapter-react-16
并配置。
/**
* setup
*
*/
import Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
Enzyme.configure({adapter: new Adapter() })
jest.config.js
能够运行 npx jest --init
在根目录生成配置文件jest.config.js
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/en/configuration.html
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// An array of directory names to be searched recursively up from the requiring module's location
moduleDirectories: ["node_modules", "src"],
// An array of file extensions your modules use
moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx"],
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: [
"./node_modules/jest-enzyme/lib/index.js",
"<rootDir>/src/utils/testSetup.js",
],
// The test environment that will be used for testing
testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// The glob patterns Jest uses to detect test files
testMatch: ["**/?(*.)+(spec|test).[tj]s?(x)"],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\/]+$"],
}
这里只是列举了罕用的配置项:
automock
: 通知 Jest 所有的模块都主动从 mock 导入.clearMocks
: 在每个测试前主动清理 mock 的调用和实例 instancecollectCoverage
: 是否收集测试时的覆盖率信息collectCoverageFrom
: 生成测试笼罩报告时检测的覆盖文件coverageDirectory
: Jest 输入笼罩信息文件的目录coveragePathIgnorePatterns
: 排除出 coverage 的文件列表coverageReporters
: 列出蕴含 reporter 名字的列表,而 Jest 会用他们来生成笼罩报告coverageThreshold
: 测试能够容许通过的阈值moduleDirectories
: 模块搜寻门路moduleFileExtensions
:代表反对加载的文件名testPathIgnorePatterns
:用正则来匹配不必测试的文件setupFilesAfterEnv
:配置文件,在运行测试案例代码之前,Jest 会先运行这里的配置文件来初始化指定的测试环境testMatch
: 定义被测试的文件transformIgnorePatterns
: 设置哪些文件不须要转译transform
: 设置哪些文件中的代码是须要被相应的转译器转换成 Jest 能辨认的代码,Jest 默认是能辨认 JS 代码的,其余语言,例如 Typescript、CSS 等都须要被转译。
匹配器
toBe(value)
:应用 Object.is 来进行比拟,如果进行浮点数的比拟,要应用 toBeCloseTonot
:取反toEqual(value)
:用于对象的深比拟toContain(item)
:用来判断 item 是否在一个数组中,也能够用于字符串的判断toBeNull(value)
:只匹配 nulltoBeUndefined(value)
:只匹配 undefinedtoBeDefined(value)
:与 toBeUndefined 相同toBeTruthy(value)
:匹配任何语句为真的值toBeFalsy(value)
:匹配任何语句为假的值toBeGreaterThan(number)
:大于toBeGreaterThanOrEqual(number)
:大于等于toBeLessThan(number)
:小于toBeLessThanOrEqual(number)
:小于等于toBeInstanceOf(class)
:判断是不是 class 的实例resolves
:用来取出 promise 为 fulfilled 时包裹的值,反对链式调用rejects
:用来取出 promise 为 rejected 时包裹的值,反对链式调用toHaveBeenCalled()
:用来判断 mock function 是否被调用过toHaveBeenCalledTimes(number)
:用来判断 mock function 被调用的次数assertions(number)
:验证在一个测试用例中有 number 个断言被调用
命令行工具的应用
在我的项目 package.json
文件增加如下script
:
"scripts": {
"start": "node bin/server.js",
"dev": "node bin/server.js",
"build": "node bin/build.js",
"publish": "node bin/publish.js",
++ "test": "jest --watchAll",
},
此时运行npm run test
:
咱们发现有以下几种模式:
f
: 只会测试之前没有通过的测试用例o
: 只会测试关联的并且扭转的文件(须要应用 git)(jest –watch 能够间接进入该模式)p
: 测试文件名蕴含输出的名称的测试用例t
: 测试用例的名称蕴含输出的名称的测试用例a
: 运行全副测试用例
在测试过程中,你能够切换适宜的模式。
钩子函数
相似于 react 或者 vue 的生命周期,一共有四种:
beforeAll()
:所有测试用例执行之前执行的办法afterAll()
:所有测试用例跑完当前执行的办法beforeEach()
:在每个测试用例执行之前须要执行的办法afterEach()
:在每个测试用例执行完后执行的办法
这里,我以我的项目中的一个根底 demo
来演示一下具体应用:
Counter.js
export default class Counter {constructor() {this.number = 0}
addOne() {this.number += 1}
minusOne() {this.number -= 1}
}
Counter.test.js
import Counter from './Counter'
const counter = new Counter()
test('测试 Counter 中的 addOne 办法', () => {counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 minusOne 办法', () => {counter.minusOne()
expect(counter.number).toBe(0)
})
运行npm run test
:
通过第一个测试用例加 1,number
的值为 1,当第二个用例减 1 的时候,后果应该是 0。然而这样两个用例间互相烦扰不好,能够通过 Jest
的钩子函数来解决。批改测试用例:
import Counter from "../../../src/utils/Counter";
let counter = null
beforeAll(() => {console.log('BeforeAll')
})
beforeEach(() => {console.log('BeforeEach')
counter = new Counter()})
afterEach(() => {console.log('AfterEach')
})
afterAll(() => {console.log('AfterAll')
})
test('测试 Counter 中的 addOne 办法', () => {counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 minusOne 办法', () => {counter.minusOne()
expect(counter.number).toBe(-1)
})
运行npm run test
:
能够清晰的看到对应钩子的执行程序:
beforeAll > (beforeEach > afterEach)(单个用例都会顺次执行) > afterAll
除了以上这些基础知识外,其实还有异步代码的测试、Mock、Snapshot 快照测试等,这些咱们会在上面 React 的单元测试示例中顺次解说。
异步代码的测试
家喻户晓,JS
中充斥了异步代码。
失常状况下测试代码是同步执行的,但当咱们要测的代码是异步的时候,就会有问题了:test case
理论曾经完结了,然而咱们的异步代码还没有执行,从而导致异步代码没有被测到。
那怎么办呢?
对于以后测试代码来说,异步代码什么时候执行它并不知道,因而解决办法很简略。当有异步代码的时候,测试代码跑完同步代码后不立刻完结,而是等完结的告诉,当异步代码执行完后再通知jest
:“好了,异步代码执行完了,你能够结束任务了”。
jest
提供了三种计划来测试异步代码,上面咱们别离来看一下。
done 关键字
当咱们的 test
函数中呈现了异步回调函数时,能够给 test
函数传入一个 done
参数,它是一个函数类型的参数。如果 test
函数传入了 done
,jest
就会等到 done
被调用才会完结以后的 test case
,如果done
没有被调用,则该 test
主动不通过测试。
import {fetchData} from './fetchData'
test('fetchData 返回后果为 { success: true}', done => {
fetchData(data => {expect(data).toEqual({success: true})
done()})
})
下面的代码中,咱们给 test
函数传入了 done
参数,在 fetchData
的回调函数中调用了 done
。这样,fetchData
的回调中异步执行的测试代码就可能被执行。
但这里咱们思考一种场景:如果应用 done
来测试回调函数(蕴含定时器场景,如setTimeout
),因为定时器咱们设置了 肯定的延时(如 3s)后执行,期待 3s 后会发现测试通过了。那如果 setTimeout
设置为几百秒,难道咱们也要在 Jest
中等几百秒后再测试吗?
显然这对于测试的效率是大打折扣的!!
jest
中提供了诸如 jest.useFakeTimers()
、jest.runAllTimers()
和toHaveBeenCalledTimes
、jest.advanceTimersByTime
等 api
来解决这种场景。
这里我也不举例具体阐明了,有这方面需要的同学能够参考 Timer Mocks
返回 Promise
⚠️ 当对
Promise
进行测试时,肯定要在断言之前加一个return
,不然没有等到Promise
的返回,测试函数就会完结。能够应用.promises/.rejects
对返回的值进行获取,或者应用then/catch
办法进行判断。
如果代码中应用了 Promise
,则能够通过返回Promise
来解决异步代码,jest
会等该 promise
的状态转为 resolve
时才会完结,如果 promise
被reject
了,则该测试用例不通过。
// 假如 user.getUserById(参数 id)返回一个 promise
it('测试 promise 胜利的状况', () => {expect.assertions(1);
return user.getUserById(4).then((data) => {expect(data).toEqual('Cosen');
});
});
it('测试 promise 谬误的状况', () => {expect.assertions(1);
return user.getUserById(2).catch((e) => {expect(e).toEqual({error: 'id 为 2 的用户不存在',});
});
});
留神,下面的第二个测试用例可用于测试 promise
返回 reject
的状况。这里用 .catch
来捕捉 promise
返回的 reject
,当promise
返回 reject
时,才会执行 expect
语句。而这里的 expect.assertions(1)
用于确保该测试用例中有一个 expect
被执行了。
对于 Promise
的状况,jest
还提供了一对匹配符resolves/rejects
,其实只是下面写法的语法糖。下面的代码用匹配符能够改写为:
// 应用 '.resolves' 来测试 promise 胜利时返回的值
it('应用'.resolves'来测试 promise 胜利的状况', () => {return expect(user.getUserById(4)).resolves.toEqual('Cosen');
});
// 应用 '.rejects' 来测试 promise 失败时返回的值
it('应用'.rejects'来测试 promise 失败的状况', () => {expect.assertions(1);
return expect(user.getUserById(2)).rejects.toEqual({error: 'id 为 2 的用户不存在',});
});
async/await
咱们晓得 async/await
其实是 Promise
的语法糖,能够更优雅地写异步代码,jest
中也反对这种语法。
咱们把下面的代码改写一下:
// 应用 async/await 来测试 resolve
it('async/await 来测试 resolve', async () => {expect.assertions(1);
const data = await user.getUserById(4);
return expect(data).toEqual('Cosen');
});
// 应用 async/await 来测试 reject
it('async/await 来测试 reject', async () => {expect.assertions(1);
try {await user.getUserById(2);
} catch (e) {expect(e).toEqual({error: 'id 为 2 的用户不存在',});
}
});
⚠️ 应用
async
不必进行return
返回,并且要应用try/catch
来对异样进行捕捉。
Mock
介绍 jest
中的 mock
之前,咱们先来思考一个问题:为什么要应用 mock
函数?
在我的项目中,一个模块的办法内经常会去调用另外一个模块的办法。在单元测试中,咱们可能并不需要关怀外部调用的办法的执行过程和后果,只想晓得它是否被正确调用即可,甚至会指定该函数的返回值。这个时候,mock
的意义就很大了。
jest
中与 mock
相干的 api
次要有三个,别离是 jest.fn()
、jest.mock()
、jest.spyOn()
。应用它们创立mock
函数可能帮忙咱们更好的测试项目中一些逻辑较简单的代码。咱们在测试中也次要是用到了 mock
函数提供的以下三种个性:
- 捕捉函数调用状况
- 设置函数返回值
- 扭转函数的外部实现
上面,我将别离介绍这三种办法以及他们在理论测试中的利用。
jest.fn()
jest.fn()
是创立 mock
函数最简略的形式,如果没有定义函数外部的实现,jest.fn()
会返回 undefined
作为返回值。
// functions.test.js
test('测试 jest.fn()调用', () => {let mockFn = jest.fn();
let res = mockFn('厦门','青岛','三亚');
// 断言 mockFn 的执行后返回 undefined
expect(res).toBeUndefined();
// 断言 mockFn 被调用
expect(mockFn).toBeCalled();
// 断言 mockFn 被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言 mockFn 传入的参数为 1, 2, 3
expect(mockFn).toHaveBeenCalledWith('厦门','青岛','三亚');
})
jest.fn()
所创立的 mock
函数还能够设置返回值,定义 外部实现
或返回 Promise 对象
。
// functions.test.js
test('测试 jest.fn()返回固定值', () => {let mockFn = jest.fn().mockReturnValue('default');
// 断言 mockFn 执行后返回值为 default
expect(mockFn()).toBe('default');
})
test('测试 jest.fn()外部实现', () => {let mockFn = jest.fn((num1, num2) => {return num1 + num2;})
// 断言 mockFn 执行后返回 20
expect(mockFn(10, 10)).toBe(20);
})
test('测试 jest.fn()返回 Promise', async () => {let mockFn = jest.fn().mockResolvedValue('default');
let res = await mockFn();
// 断言 mockFn 通过 await 关键字执行后返回值为 default
expect(res).toBe('default');
// 断言 mockFn 调用后返回的是 Promise 对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
jest.mock()
个别在实在的我的项目里,测试异步函数的时候,不会真正的发送 ajax
申请去申请这个接口,为什么?
比方有 1w
个接口要测试,每个接口要 3s
能力返回,测试全副接口须要 30000s
,那么这个自动化测试的工夫就太慢了
咱们作为前端只须要去确认这个异步申请发送胜利就好了,至于后端接口返回什么内容咱们就意外了,这是后端自动化测试要做的事件。
这里以一个 axios 申请
的demo
为例来阐明:
// user.js
import axios from 'axios'
export const getUserList = () => {return axios.get('/users').then(res => res.data)
}
对应测试文件user.test.js
:
import {getUserList} from '@/services/user.js'
import axios from 'axios'
// 👇👇
jest.mock('axios')
// 👆👆
test.only('测试 getUserList', async () => {axios.get.mockResolvedValue({ data: ['Cosen','森林','柯森'] })
await getUserList().then(data => {expect(data).toBe(['Cosen','森林','柯森'])
})
})
咱们在测试用例的最下面退出了 jest.mock('axios')
,咱们让jest
去对 axios
做模仿,这样就不会去申请真正的数据了。而后调用 axios.get
的时候,不会实在的申请这个接口,而是会以咱们写的 {data: ['Cosen','森林','柯森'] }
去模仿申请胜利后的后果。
当然模仿异步申请是须要工夫的,如果申请多的话工夫就很长,这时候能够在本地
mock
数据,在根目录下新建__mocks__
文件夹。这种形式就不必去模仿axios
,而是间接走的本地的模仿办法,也是比拟罕用的一种形式,这里就不开展阐明了。
jest.spyOn()
jest.spyOn()
办法同样创立一个 mock
函数,然而该 mock
函数不仅可能捕捉函数的调用状况,还能够失常的执行被 spy
的函数。实际上,jest.spyOn()
是 jest.fn()
的语法糖,它创立了一个和被 spy
的函数具备雷同外部代码的mock 函数
。
Snapshot 快照测试
所谓snapshot
,即快照也。通常波及 UI 的自动化测试,思路是把某一时刻的标准状态拍个快照。
describe("xxx 页面", () => {// beforeEach(() => {// jest.resetAllMocks()
// })
// 应用 snapshot 进行 UI 测试
it("页面应能失常渲染", () => {const wrapper = wrappedShallow()
expect(wrapper).toMatchSnapshot()})
})
当应用 toMatchSnapshot
的时候,Jest
将会渲染组件并创立其快照文件。这个快照文件蕴含渲染后组件的整个构造,并且应该与测试文件自身一起提交到代码库。当咱们再次运行快照测试时,Jest
会将新的快照与旧的快照进行比拟,如果两者不统一,测试就会失败,从而帮忙咱们确保用户界面不会发生意外扭转。
🎯 总结
到这里,对于前端单元测试的一些根底背景和 Jest
的根底 api
就介绍完了,在下一篇文章中,我会联合我的项目中的一个 React 组件
来解说如何做 组件单元测试
。
📜 参考链接
- https://segmentfault.com/a/11…