Vitest下一代测试框架
Vitest官网:cn.vitest.dev/
stackblitz在线IDE:stackblitz.com/
Vitest介绍
随着前端技术的不断发展,前端需要做的内容逐渐复杂,如何让自己的代码更加健壮这是当今前端开发人员需要考虑的问题,我相信大多数人都使用debugger
或log
测试代码执行结果是否符合预期值,这两种方法有很多的局限性。我们需要使用单元测试
工具来满足复杂的测试需求,这里业内常用的工具有vitest
和jest
。
进入官网映入眼帘的就是Vitest
的以下四个优势,我们可以在编写测试代码时来体验Vitest
来带给我们的便利以及它基于Vite
驱动所带来的编译速度。
Vitest项目创建
在将要开始前,在这里推荐一个在线的IDE平台stackblitz
,这样就可以节省我们搭建项目的时间了。创建项目如下:
创建完成后可以看到package.json
文件中有有三种运行项目的命令,这里推荐使用test:ui
这里它会给我们启动一个ui页面以便于观察每个测试任务的运行结果。
js
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run"
},
编写测试代码
测试代码的文件名称必须是.test
或.spec
的名称才能被vitest
识别成测试文件。我们来编写一个简单的测试代码。
ts
// src/index.ts
export function sum(a:number, b:number) { return a + b }
ts
// test/index.test.ts
import { expect, test } from 'vitest';
import { sum } from '../src/sum.ts';
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
可以看到运行的页面有一条成功
我们将toBe(3)
改成toBe(4)
时则页面中有一条失败,从中我们可以看到预期的值是4 结果是3。
test API
test
test
函数是定义一条测试,它接收测试的描述和测试执行的函数。或者我们可以来指定测试代码的超时时间(毫秒),默认是5s 。以下测试代码还是执行成功了,我也不知道为什么有用过的可以在评论区解释一下。
ts
test(
'超时时间测试',
() => {
speed(10);
console.log('asd');
},
{ timeout: 5000 }
);
function speed(time: number) {
let startTime = new Date().getTime();
let endTime = startTime + time * 1000;
while (endTime > startTime) {
startTime = new Date().getTime();
}
}
test.extend
test.extend
它会返回一个新的test
用来自定装置来设置自己test
API,可以在任何地方复用它。当我们使用自定义test
里的参数时它会根据参数名称去extend
方法里寻找对应的函数并执行此函数,函数中有use
方法,当调用use
方法时这个变量的值已经确认了。就算use
方法之后做修改也不会改变。
ts
const myTest = test.extend({
a: async ({}, use)=>{
let a = {name:'vitest'};
await use(a);
a.name = 'jest';
}
})
myTest('测试extends传过来a变量',({a})=>{
console.log(a); // {name:'vitest'}
}
应用场景
假如我们代码中有很多函数需要两个随机的数字来进行操作的,例如sum
函数等,我们就可以使用extend
来自定义一个测试,它会给我们两个随机数的变量。
myTest
被抽离出来并导出,这样我们需要使用两个随机数变量时就可以引入myTest
测试方法。
ts
// test/extend/myTest.ts
export const myTest = test.extend({
a: async ({}, use) => {
const a = Math.floor(Math.random() * 100);
await use(a);
},
b: async ({}, use) => {
const b = Math.floor(Math.random() * 100);
await use(b);
},
});
ts
myTest('随机生成两个变量', ({ a, b }) => {
expect(sum(a, b)).toBe(a + b);
});
test.skip
可以跳过运行某些测试,我们不想注释代码就可以使用这种方法。
ts
test.skip('跳过这个测试',()=>{})
可以通过context
动态调用skip
跳过测试,skip
以下的代码就不会执行。
ts
test('动态跳过测试',(context)=>{
expect(sum(1,1)).toBe(2);
context.skip(); // 跳过下面的代码
expect(sum(1,2)).toBe(3); // 不会执行,也不会提示测试失败
})
test.concurrent
test.concurrent
并行运行测试代码,多个test
测试代码时,它会按照顺序执行,必须要等到上一个测试任务成功或失败时才会执行下一个测试任务。
运行一下代码来验证效果
js
test('concurrent test 1', async () => {
await speed();
});
test('concurrent test 2', async () => {
});
function speed() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}
以上代码可以看到当test1
执行成功之后才会接着执行test2
的测试任务。当我们使用concurrent
时就可以很明显看到test2
直接完成而test1
还在执行中,这就是concurrent
函数的效果。
test.sequential
test.sequential
制定一个测试为顺序测试。vitest
中默认的执行顺序就是是顺序执行,但是当你使用concurrent
或者shuffle
时则就是打乱test
的执行,我们就可以使用sequential
来确保测试任务的执行顺序。
ts
describe.concurrent('describe', () => {
test.sequential('test1', async () => {
await speed();
console.log('tet1');
})
test.sequential('test2', () => {
console.log('test2');
})
})
可以看到test2
会等待test1
执行完毕。
test.each
当需要使用不同变量运行同一个测试时,可以使用test.each
方法。还是以sum
方法来举例,我们需要使用多条数据来测试sum
方法的健壮性。
ts
test.each([
{ a: 1, b: 2, result: 3 },
{ a: 10, b: 20, result: 30 },
{ a: 5, b: 6, result: 11 },
{ a: 1, b: 2, result: 4 },
])('each test methods', ({ a, b, result }) => {
expect(sum(a, b)).toBe(result);
});
可以看到有一条错误信息。
剩下的test相关的API可以去参考文档进行阅读,这里就不做过多的介绍了
batch API
个人感觉好像batch
还没有集成到vitest
中,在网上看了很多没都没有找到答案。
describe API
describe
是为了将多个test
收集起来形成一个新的测试套件,测试套件可让组织测试和基准,使报告更加清晰。
ts
describe('describe', () => {
test('test1', () => {});
test('test2', () => {});
test('test3', () => {});
});
describe
扩展的API
和test
扩展的API是相同的,所实现的工具也是一样的,这里就简单介绍一些比较难以理解的API
。
describe.concurrent
从上面的test
就可以看出,这个也是并发
的功能,但是需要注意的是它里面所有的test
测试都是并行的。
ts
describe.concurrent('describe', () => {
test('concurrent test1', () => {});
test.concurrent('concurrent test2', () => {}); // 加了concurrent也是相同的
test('concurrent test3', () => {});
});
describe.shuffle
这个方法是在test
中没有的,它主要是提供一种随机运行所有的测试方法,不会按照代码执行顺序来运行测试方法。直接在ui
上看不出执行顺序的效果,使用vscode
来进行断点调试来看看。
ts
describe.shuffle('describe', () => {
test('test1', async () => {
expect(sum(1, 2)).toBe(3)
});
test('test2', () => {
expect(sum(100, 200)).toBe(300)
});
test('test3', () => {
expect(sum(10000, 20000)).toBe(30000)
});
})
通过断点调试就可以看出来test2
先执行了。
生命周期
beforEach
当前上下文中每个测试之前执行,如果是一个异步任务
时会等待执行完成。
ts
test('test1', () => {
console.log('test1');
});
test('test2', () => {
console.log('test2');
});
beforeEach(() => {
console.log('beforeEach');
});
可以看到beforeEach
在每一个test
之前打印。当我们在beforEach
中使用异步任务
时它会是一个怎样的效果呢?
ts
test('test1', () => {
console.log('test1');
});
beforeEach(async () => {
await speed();
console.log('beforeEach');
});
function speed() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
});
}
它会等待beforEach
的执行,当执行完毕时才会去执行test
函数。
那要是如果给test
添加concurrent
函数时又会是怎样的呢?其实和不加是一样的效果,因为concurrent
是指当前的test
是一个并行测试并不是beforEach
会并行。
afterEach
有before
就会有after
,它的功能就是和before
相反,它是在测试执行完成之后调用的。
ts
test('test1', () => {
console.log('test1');
});
afterEach(() => {
console.log('afterEach');
});
beforeAll
在当前测试上下文中所有测试开始之前调用一次,回调函数中如果有异步任务
和beforEach
一样的效果。
afterAll
在当前测试上下文中所有测试结束之后调用一次。
expect API
expect
用来创建断言,通俗的来讲就是来判断函数执行的结果是否符合你的预期,这是在单元测试
中最常用的API
。
例如我想判断sum
函数的执行结果是否符合我们的预期值。
ts
expect(sum(1, 2)).toBe(2)
接下来简单介绍一些下面测试组件时所用的API
。
测试组件所需要的API
vi.fn
创建一个虚拟函数用于组件函数的执行。
ts
const fn = vi.fn();
toHaveBeenCalled
断言函数是否执行。
ts
expect(fn).toHaveBeenCalled() // 执行了
expect(fn).not.toHaveBeenCalled() // 没有执行
toEqual
断言是否和预期值相等,和toBe
的差别就是在于toBe
是严格判断是相等的而toEqual
是深度遍历是否相等
ts
test('toBe or toEqual', () => {
expect(obj).toBe(user);
expect(obj).toEqual(user);
expect(obj).toEqual({ name: 'jack' });
expect(obj).toBe({ name: 'jack' });
})
arrayContaining
断言数组中是否包含制定项,通过配合toEqual
一起使用。
ts
test('basket includes fuji', () => {
const basket = {
varieties: ['Empire', 'Fuji', 'Gala'],
count: 3,
}
expect(basket).toEqual({
count: 3,
varieties: expect.arrayContaining(['Fuji']),
})
})
Vitest测试React组件
我们必须要使用vite
来创建React
应用,vitest
是基于vite
来实现的。
kotlin
npm init vite@latest react-vitest-app
项目创建完成后我们需要安装一些测试组件所需要的第三方依赖。
dart
npm install vitest jsdom @testing-library/react -D
这里测试react
组件我们使用@testing-library/react
这个库。从npm
的数据来看这个库的周下载量已经是惊人900多万,足以可以证明这个库的火热程度。
修改vite.config.ts
文件,加入如下几行配置rollup
才能处理测试文件。
ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
coverage: {
reporter: ['text', 'json', 'html']
}
}
})
修改package.json
文件在scripts
添加test
运行命令。
json
"test": "vitest",
下面我们写一个todoList
的组件来测试一下流程。
创建一个ToDoHeader.tsx
的组件,代码如下:
tsx
import { useState } from "react";
interface TodoHeaderProps {
add: (value: string) => void;
}
const ToDoHeader: React.FC<TodoHeaderProps> = props => {
const [value, setValue] = useState<string>("");
const addToDoItem = () => {
if (value) {
props.add(value);
setValue("");
}
};
return (
<div className="header">
<input
aria-label="toDoInput"
type="text"
value={value}
onInput={(event: React.ChangeEvent<HTMLInputElement>) => setValue(event.target.value)}
/>
<button role="toDoBtn" onClick={addToDoItem}>
添加
</button>
</div>
);
};
export default ToDoHeader;
添加ToDoList.tsx
组件,代码如下:
tsx
import React from "react";
interface ToDoListProps {
list: Array<string>;
setList: React.Dispatch<React.SetStateAction<string[]>>;
}
const ToDoList: React.FC<ToDoListProps> = ({ list, setList }) => {
const delToDoItem = (index: number) => {
list.splice(index, 1);
setList([...list]);
};
return (
<ul className="list">
{list.map((item, index) => {
return (
<li key={index}>
<span>{item}</span>
<button onClick={() => delToDoItem(index)}>删除</button>
</li>
);
})}
</ul>
);
};
export default ToDoList;
在App.tsx
中使用这两个组件,代码如下:
tsx
import { useState } from "react";
import ToDoHeader from "./components/ToDoHeader";
import ToDoList from "./components/ToDoList";
function App() {
const [list, setList] = useState([]);
const add = (value: string) => {
setList([...list, value]);
};
return (
<div className="container">
<ToDoHeader add={add} />
<ToDoList list={list} setList={setList} />
</div>
);
}
export default App;
以上就是一个很简单的todoList
的组件功能,代码还是非常简单的,接下来我们创建ToDoList.spec.tsx
文件来测试一下组件中的所有功能。
ToDoList.spec.tsx
测试代码如下,这里测试一下一些功能。
- 输入框没有输入值,是否会进行添加
- 输入框输入值,是否会进行添加
- 列表是否渲染了
- 点击删除按钮数组中是否删除了
tsx
import React, { useState } from "react";
import { describe, test, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import ToDoHeader from "../src/components/ToDoHeader";
import TodoList from "../src/components/ToDoList";
let list = ["vitest", "jest", "vite"];
const setList = vi.fn(value => {
list = value;
});
const toDoHeaderSetup = () => {
const handleCallback = vi.fn(value => {
// 添加todo
setList([...list, value]);
});
// 渲染组件
const container = render(<ToDoHeader add={handleCallback} />);
// 获取input DOM
const toDoInput = container.getByLabelText("toDoInput");
// 获取button DOM
const toDoBtn = container.getByRole("toDoBtn");
return {
toDoBtn,
toDoInput,
handleCallback,
};
};
describe("ToDoHeader test", () => {
test("文本框没有内容点击添加", () => {
const { toDoBtn, handleCallback } = toDoHeaderSetup();
fireEvent.click(toDoBtn);
// 判断props中的add函数是否执行
expect(handleCallback).not.toHaveBeenCalled();
});
test("文本框有内容点击添加", () => {
const { toDoInput, toDoBtn, handleCallback } = toDoHeaderSetup();
// 设置input的value值
fireEvent.input(toDoInput, { target: { value: "webpack" } });
// 点击事件
fireEvent.click(toDoBtn);
// 判断props中的add函数是否执行
expect(handleCallback).toHaveBeenCalled();
});
});
const todoListSetup = () => {
const container = render(<TodoList list={list} setList={setList} />);
// 获取第一个节点
const todoItem = container.getByText(list[0]);
return {
container,
list,
setList,
todoItem,
};
};
describe("ToDoList test", () => {
test("列表初始化是否渲染", () => {
const { container, todoItem } = todoListSetup();
// 断言li个数
expect(container.container.firstElementChild?.childNodes.length).toBe(list.length);
// 获取下一个兄弟节点,这就是button
const delBtnItem = todoItem.nextElementSibling as HTMLElement;
// 点击事件
fireEvent.click(delBtnItem);
// 删除函数是否执行
expect(setList).toHaveBeenCalled();
// 列表断言个数是否减少
expect(list.length).toBe(3);
});
});
完成测试代码后,运行npm run vitest
可以看到所有的测试任务执行成功,todoList
的组件也就没有任何问题了。
运行项目,页面功能也是没有任何问题。
Vitest测试Vue组件
使用vite
创建vue3
应用。
kotlin
npm init vite@latest vue-vitest-app
项目创建完成后我们需要安装一些测试组件所需要的第三方依赖。
bash
npm install vitest @vue/test-utils happy-dom -D
这是我们测试vue
组件使用@vue/test-utils
这个第三方库。从npm上的数据可以看到这个库的周下载量也是100多万,在vue
组件测试中也是很火热的一个测试库。
修改vite.config.ts
文件如下。这里@vue/test-utils
构建出来的是happy-dom
的形式,所以需要给vitest
来说明一下。之前react
组件测试时构建出来的是js-dom
的形式
ts
test: {
globals: true,
environment: 'happy-dom',
coverage: {
reporter: ['text', 'json', 'html']
}
}
修改package.json
文件如下,在scripts
中添加test
命令。
json
"test":"vitest",
Button
组件代码如下:
html
<script lang="ts" setup>
const props = defineProps({
type: { type: String, default: "primary" },
});
</script>
<template>
<button :class="`btn-${props.type}`">
<span>
<slot>按钮</slot>
</span>
</button>
</template>
<style scoped>
button {
border: none;
border-radius: 4px;
color: #fff;
padding: 8px 15px;
cursor: pointer;
}
.btn-primary {
background-color: #409eff;
}
.btn-success {
background-color: #67c23a;
}
.btn-info {
background-color: #909399;
}
.btn-warning {
background-color: #e6a23c;
}
.btn-danger {
background-color: #f56c6c;
}
</style>
创建一个button.spec.ts
测试文件,代码如下。
ts
import { test, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../src/components/Button.vue'
describe('Button component test', () => {
test.each([
{ type: '', text: '默认按钮', className: 'btn-primary' },
{ type: 'primary', text: '主要按钮', className: 'btn-primary' },
{ type: 'success', text: '成功按钮', className: 'btn-success' },
{ type: 'info', text: '信息按钮', className: 'btn-info' },
{ type: 'danger', text: '危险按钮', className: 'btn-danger' },
{ type: 'warning', text: '警告按钮', className: 'btn-warning' },
])('测试Button组件样式是否正确', ({ type, text, className }) => {
const element = mount(Button, {
props: type ? { type: type } : {},
slots: {
default: text
}
});
// 检查class名称是否包含className
expect(element.classes()).toEqual(expect.arrayContaining([className]))
// 查看插槽渲染是否正确
expect(element.text()).toBe(text);
})
})
执行npm run vitest
测试任务全部通过。
运行项目,效果也一切正常。
这是我的第一遍文章,对于单元测试也是只有基本的了解,有哪些理解不对地方还请你们纠正,本人觉得写单侧代码的时间已经可以把组件自测好几遍的了,个人感觉没什么必要。,如果还想了解更深入的用法我也会继续更新或着去看官方文档,这里也就介绍了一些基本的API
文档中还有需要expect API
的使用,就说到这里了。