We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
npm install -D @babel/core @babel/preset-env
.babelrc
{ "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] }
jest 在运行前会检查是否安装 babel,如果安装了会去取 .babelrc 文件,结合 babel 将代码进行转化,运行转化后的代码。 3. jest 默认配置
npx jest --init
git
const throwError = () => { throw new Error('error') } it('can throw error', () => { expect(throwError).toThrow('error') // 判断throw函数可以抛出异常,异常信息为 "error"。也可以写正则 })
这里有个小技巧:当我们想忽略掉单个文件中的其他测试用例,只针对一个测试用例做调试的时候,可以加上 .only
.only
it.only('test', () => { // ... })
但这并不会忽略其他测试文件的测试用例
这里有三个异步方法,对这三个方法进行代码测试,"http://www.dell-lee.com/react/api/demo.json" 会返回 {success: true}, "http://www.dell-lee.com/react/api/404.json" 则不存在。
import axios from 'axios' export function getData1() { return axios.get('http://www.dell-lee.com/react/api/demo.json') } export function getData2(fn) { axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => { fn(res) }) } export function get404() { return axios.get('http://www.dell-lee.com/react/api/404.json') }
对于异步代码测试,时机很重要,必须保证我们的测试用例在异步代码走完之后才结束。有以下几种办法:
import {getData1, getData2, get404} from './fetchData/fetchData' it('getData1 方法1', (done) => { getData1().then(res => { expect(res.data).toEqual({ success: true }) done() // 如果不加 done,还没执行到 .then 方法,测试用例已经结束了 }) }) it('getData1 方法2', () => { return getData1().then(res => { expect(res.data).toEqual({ success: true }) }) }) it('getData2 方法2', (done) => { getData2((res) => { expect(res.data).toEqual({ success: true }) done() }) }) it('getData1 方法3', async () => { const res = await getData1() expect(res.data).toEqual({ success: true }) }) /*********** 重点关注 ***********/ it('get404', (done) => { expect.assertions(1) get404().catch(r => { expect(r.toString()).toMatch('404') done() }) })
重点讲一下上面的最后一个测试用例,假设我们现在有一个返回的是 404 的接口,我们需要对这个接口测试,期望他返回 404。 我们用 catch 捕获,在 catch 中判断。
但是,假如这个接口返回的不是 404,而是正常返回 200,这个 catch 则不会执行,expect 也不会执行,测试依然是通过的。这不符合我们的预期!所以,我们需要加上 expect.assertions(1) 进行断言:下面一定会执行一个 expect
expect.assertions(1)
当然,也可以用 async await 方法进行 404 接口的测试
it('get404 方法3', async () => { await expect(get404()).rejects.toThrow() })
前四个钩子使用起来很简单,调用方法如下:
beforeAll(() => { // ... })
如果测试前后要做一些处理,尽可能写在这些钩子函数中,他能保证一定的执行顺序。
describe 可以用来进行用例分组,为了让我们的测试输出结果更好看,更有层次。 同时,在每个 describe 中都有上面 4 个钩子函数的存在,我们来看看具体的情况:
describe('测试 Button 组件', () => { beforeAll(...) // 1 beforeEach(...) // 2 afterEach(...) // 3 afterAll(...) // 4 describe('测试 Button 组件的事件', () => { beforeAll(...) // 5 beforeEach(...) // 6 afterEach(...) // 7 afterAll(...) // 8 it('event1', ()=>{...}) }) })
上面钩子函数的执行顺序是: 1 > 5 > 2 > 6 > 3 > 7 > 4 > 8 外部的钩子函数对 describe 内部的用例也生效,执行顺序为:先外部后内部
前面提到了可以测试异步代码,对于一些接口都能进行请求测试。但假如每一个接口都真的发起请求,那一次测试需要耗费的时间是很多的。 这时候我们可以模拟请求方法,步骤如下:
import axios from 'axios' export function getData() { return axios.get('http://www.dell-lee.com/react/api/demo.json') }
export function getData() { return Promise.resolve({ success: true }) }
jest.mock('./mock/mock.js') // 用 jest 模拟 mock.js 方法 import {getData} from './mock/mock.js' // 导入 mock.js,但实际上 jest 会导入 __mocks__ 下的 mock.js test('mock 方法测试', () => { getData().then(data => { expect(data).toEqual({ success: true }) done() }) })
除了上面的这种办法,还能在 jest.config.js 中配置自动开启 mock,这样 jest 会自动去查找当前文件同级有没有 mock 文件夹,里面有没有对应文件
module.exports = { automock: true }
讲了两种 mock 的方法,还有一种极端情况需要避免 mock: 我们在 mock.js 中定义了一个需要 mock 的 getData 方法,又另外定义了一个不需要 mock 的普通方法,当我们在测试文件导入的时候,需要避免 jest 去 mocks/mock.js 下找这个普通方法,这里需要用 jest 提供的方法导入:
const { regularMethod } = jest.requireActual('./mock/mock.js')
当我们有如下代码需要测试的时候:
export default (fn) => { setTimeout(() => { fn() }, 3000) }
我们不可能总是去等待定时器,这时候我们要用 Jest 来操作时间!步骤如下:
jest.useFakeTimers()
jest.advanceTimersByTime(3000)
特别说明一下:jest.fn() 生成的是一个函数,这个函数能被监听调用过几次。
import timer from './timer/timer' beforeEach(() => { jest.useFakeTimers() }) it('timer 测试', () => { const fn = jest.fn() timer(fn) jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(1) })
同样的,当我们只关注类的方法是否被调用,而不关心方法调用产生的结果时,可以 mock 类
在 util/util.js 中定义了 Util 类
export class Util { a() {} b() {} }
在 util/useUtil 中调用了这个类
import {Util} from './util' export function useUtil() { let u = new Util() u.a() u.b() }
我们需要测试 u.a 和 u.b 被调用,jest.mock('./util/util') 会将 Util、Util.a、Util.b 都 mock 成 jest.fn
jest.mock('./util/util')
测试用例如下:
jest.mock('./util/util') // mock Util 类 import {Util} from './util/util' import {useUtil} from './util/uesUtil' test('util 的实例方法被执行了', () => { useUtil() expect(Util).toHaveBeenCalled() expect(Util.mock.instances[0].a).toHaveBeenCalled() expect(Util.mock.instances[0].b).toHaveBeenCalled() })
Vue 提供了 @vue/test-utils 来帮助我们进行单元测试,创建 Vue 项目的时候勾选测试选项会自动帮我们安装。
先来介绍两个常用的挂载方法:
再来看一个简单的测试用例:
import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue' describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { propsData: { msg } }) expect(wrapper.props('msg')).toBe(msg) }) })
shallowMount 会返回一个 wrapper,这个 wrapper 上面会包含很多帮助我们测试的方法,详见
快照测试的意思是,会将组件像拍照一样拍下来,存底。下次运行测试用例的时候,如果组件发生变化,和快照不一样了,就会报错。
测试用例写法如下: 第一次测试会保存 wrapper 的快照,第二次会比较当前 wrapper 和快照的区别
describe('HelloWorld.vue', () => { it('renders props.msg when passed', () => { const msg = 'new message' const wrapper = shallowMount(HelloWorld, { propsData: { msg } }) expect(wrapper).toMatchSnapshot() }) })
我们再来看看快照长什么样子: 可以看到,快照实际保存的就是组件渲染之后的 html 部分,css 部分没有保存,在元素上绑定的 @click 等一些事件也不会保存, 所以快照适合进行 DOM 节点是否变化的测试。
当快照发生变化时,我们可以在终端按 u 进行更新快照
u
覆盖率测试是对测试完全程度的一个评估,测试覆盖到的业务代码越多,覆盖率越高。
在 jest.config.js 中我们可以设置 collectCoverageFrom,来设置需要进行覆盖率测试的文件,这里我们测试一下所有的 .vue 文件,忽略 node_modules 下所有文件。
collectCoverageFrom
.vue
node_modules
要注意,在 Vue 中配置 jest,参考文档
然后添加一条 script 命令,就能进行测试了:
"test:unit": "vue-cli-service test:unit --coverage"
执行命令会生成 coverage 文件夹,Icov-report/index.html 里会可视化展示我们的测试覆盖率
coverage
Icov-report/index.html
就拿 shallowMount 来说,这个 api 就很适合单元测试,单元测试不关注单元之间的联系,对每个单元进行独立测试, 这也使得它代码量大,测试间过于独立。在进行一些函数库的测试,各个函数比较独立的时候,就很适合单元测试。 在进行一些业务组件测试时,需要关注组件间的联系,比较适合用集成测试。
shallowMount
TDD:测试驱动开发,先写测试用例,然后根据用例写代码,比较关注代码本身。如下:
describe('input 输入回车,向外触发事件,data 中的 inputValue 被赋值', () => { const wrapper = shallowMount(TodoList) const inputEle = wrapper.find('input').at(0) const inputContent = '用户输入内容' inputEle.setValue(inputContent) // expect:add 事件被 emit except(wrapper.emitted().add).toBeTruthy() // expect:data 中的 inputValue 被赋值为 inputContent except(wrapper.vm.inputValue).toBe(inputContent) })
TDD 关注代码内部如何实现,关注事件是否触发?属性是否设置?data 数据是否被更新?
BDD:用户行为驱动开发,先写完业务代码,然后站在用户的角度去测试功能,不关注代码实现过程,只是通过模拟用户操作测试功能。 比如下面这个用例:
describe('TodoList 测试', () => { it(` 1. 用户在 header 输入框输入内容 2. 键盘回车 3. 列表项增加一项,内容为用户输入内容 `, () => { // 挂载 TodoList 组件 const wrapper = mount(TodoList) // 模拟用户输入 const inputEle = wrapper.find('input').at(0) const inputContent = '用户输入内容' inputEle.setValue(inputContent) // 模拟触发的事件 inputEle.trigger('content') inputEle.trigger('keyup.enter') // expect:列表项增加对应内容 const listItems = wrapper.find('.list-item') expect(listItems.length).toBe(1) // 增加 1 项 expect(listItems.at(0).text()).toContain(inputContent) // 增加 1 项 }) })
The text was updated successfully, but these errors were encountered:
No branches or pull requests
一、Jest 简介
速度快、API简单、配置简单
Jest 不支持 ES Module 语法,需要安装 babel
.babelrc
jest 在运行前会检查是否安装 babel,如果安装了会去取 .babelrc 文件,结合 babel 将代码进行转化,运行转化后的代码。
3. jest 默认配置
git
结合使用,会比较现有文件和 commit 的文件的差异,只测试差异文件二、Jest 匹配器
常见匹配器
Number 相关
String 相关
Array Set 相关
异常匹配器
这里有个小技巧:当我们想忽略掉单个文件中的其他测试用例,只针对一个测试用例做调试的时候,可以加上
.only
但这并不会忽略其他测试文件的测试用例
三、测试异步代码
这里有三个异步方法,对这三个方法进行代码测试,"http://www.dell-lee.com/react/api/demo.json" 会返回 {success: true},
"http://www.dell-lee.com/react/api/404.json" 则不存在。
对于异步代码测试,时机很重要,必须保证我们的测试用例在异步代码走完之后才结束。有以下几种办法:
重点讲一下上面的最后一个测试用例,假设我们现在有一个返回的是 404 的接口,我们需要对这个接口测试,期望他返回 404。
我们用 catch 捕获,在 catch 中判断。
但是,假如这个接口返回的不是 404,而是正常返回 200,这个 catch 则不会执行,expect 也不会执行,测试依然是通过的。这不符合我们的预期!所以,我们需要加上
expect.assertions(1)
进行断言:下面一定会执行一个 expect当然,也可以用 async await 方法进行 404 接口的测试
四、Jest 中的一些钩子函数
前四个钩子使用起来很简单,调用方法如下:
如果测试前后要做一些处理,尽可能写在这些钩子函数中,他能保证一定的执行顺序。
describe 可以用来进行用例分组,为了让我们的测试输出结果更好看,更有层次。
同时,在每个 describe 中都有上面 4 个钩子函数的存在,我们来看看具体的情况:
上面钩子函数的执行顺序是:
1 > 5 > 2 > 6 > 3 > 7 > 4 > 8
外部的钩子函数对 describe 内部的用例也生效,执行顺序为:先外部后内部
五、Jest 中的 mock
1. 在 Jest 中 mock 异步方法
前面提到了可以测试异步代码,对于一些接口都能进行请求测试。但假如每一个接口都真的发起请求,那一次测试需要耗费的时间是很多的。
这时候我们可以模拟请求方法,步骤如下:
这里我们直接返回一个 Promise,把假数据 resolve 出去
这里有一个需要注意的坑:jest.mock 不能写在任何钩子函数里,因为钩子函数的执行时机问题,beforeAll 也不行,当钩子函数执行时,没有写在钩子函数里面的代码已经执行了,也就是已经 import 了!
除了上面的这种办法,还能在 jest.config.js 中配置自动开启 mock,这样 jest 会自动去查找当前文件同级有没有 mock 文件夹,里面有没有对应文件
讲了两种 mock 的方法,还有一种极端情况需要避免 mock:
我们在 mock.js 中定义了一个需要 mock 的 getData 方法,又另外定义了一个不需要 mock 的普通方法,当我们在测试文件导入的时候,需要避免 jest 去 mocks/mock.js 下找这个普通方法,这里需要用 jest 提供的方法导入:
2. 用 Jest 操控时间
当我们有如下代码需要测试的时候:
我们不可能总是去等待定时器,这时候我们要用 Jest 来操作时间!步骤如下:
jest.useFakeTimers()
使用 jest “自制的” 定时器,这里放在 beforeEach 里面是因为快进时间可能被调用多次,我希望在每个测试用例里,这个时钟都是初始状态,不会互相影响。jest.advanceTimersByTime(3000)
,这个方法可以调用任意次,快进的时间会叠加。特别说明一下:jest.fn() 生成的是一个函数,这个函数能被监听调用过几次。
3. mock 类
同样的,当我们只关注类的方法是否被调用,而不关心方法调用产生的结果时,可以 mock 类
在 util/util.js 中定义了 Util 类
在 util/useUtil 中调用了这个类
我们需要测试 u.a 和 u.b 被调用,
jest.mock('./util/util')
会将 Util、Util.a、Util.b 都 mock 成 jest.fn测试用例如下:
六、结合 Vue组件 进行单元测试
1. 简单用例入门
Vue 提供了 @vue/test-utils 来帮助我们进行单元测试,创建 Vue 项目的时候勾选测试选项会自动帮我们安装。
先来介绍两个常用的挂载方法:
再来看一个简单的测试用例:
shallowMount 会返回一个 wrapper,这个 wrapper 上面会包含很多帮助我们测试的方法,详见
2. 快照测试
快照测试的意思是,会将组件像拍照一样拍下来,存底。下次运行测试用例的时候,如果组件发生变化,和快照不一样了,就会报错。
测试用例写法如下:
第一次测试会保存 wrapper 的快照,第二次会比较当前 wrapper 和快照的区别
我们再来看看快照长什么样子:
可以看到,快照实际保存的就是组件渲染之后的 html 部分,css 部分没有保存,在元素上绑定的 @click 等一些事件也不会保存,
所以快照适合进行 DOM 节点是否变化的测试。
当快照发生变化时,我们可以在终端按
u
进行更新快照覆盖率测试
覆盖率测试是对测试完全程度的一个评估,测试覆盖到的业务代码越多,覆盖率越高。
在 jest.config.js 中我们可以设置
collectCoverageFrom
,来设置需要进行覆盖率测试的文件,这里我们测试一下所有的.vue
文件,忽略node_modules
下所有文件。要注意,在 Vue 中配置 jest,参考文档
然后添加一条 script 命令,就能进行测试了:
执行命令会生成
coverage
文件夹,Icov-report/index.html
里会可视化展示我们的测试覆盖率七、写在最后
1. 单元测试 or 集成测试?
就拿
shallowMount
来说,这个 api 就很适合单元测试,单元测试不关注单元之间的联系,对每个单元进行独立测试,这也使得它代码量大,测试间过于独立。在进行一些函数库的测试,各个函数比较独立的时候,就很适合单元测试。
在进行一些业务组件测试时,需要关注组件间的联系,比较适合用集成测试。
2. TDD or BDD?
TDD:测试驱动开发,先写测试用例,然后根据用例写代码,比较关注代码本身。如下:
TDD 关注代码内部如何实现,关注事件是否触发?属性是否设置?data 数据是否被更新?
BDD:用户行为驱动开发,先写完业务代码,然后站在用户的角度去测试功能,不关注代码实现过程,只是通过模拟用户操作测试功能。
比如下面这个用例:
The text was updated successfully, but these errors were encountered: