Vitest即将成为下一代最流行的前端单测框架

Vitest下一代测试框架

Vitest官网:cn.vitest.dev/

stackblitz在线IDE:stackblitz.com/

Vitest介绍

随着前端技术的不断发展,前端需要做的内容逐渐复杂,如何让自己的代码更加健壮这是当今前端开发人员需要考虑的问题,我相信大多数人都使用debuggerlog测试代码执行结果是否符合预期值,这两种方法有很多的局限性。我们需要使用单元测试工具来满足复杂的测试需求,这里业内常用的工具有vitestjest

进入官网映入眼帘的就是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用来自定装置来设置自己testAPI,可以在任何地方复用它。当我们使用自定义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扩展的APItest扩展的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的使用,就说到这里了。

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js