前言

Vitest 是一款配搭 Vite 的前端单元测试工具,可以用于取代 JasmineJest

我先聊一下测试,每当添加新代码或修改旧代码后,我们多少都得测试一下,以确保功能正确才能交付。

这种测试通常只是写几个简单的调用,换换参数,console 看看输出。没有问题也就 ok 了。

大部分情况下并不需要用到工具。但是如果我们经常要添加或修改同一份代码的话(比如维护一个库),这种测试就变得非常繁琐,

每改一次就得重新测一遍,多麻烦啊,这时就需要一些测试工具来帮忙了。

测试工具的工作就是把我们上面原本随意写的调用,换参数,console 变成一种规范,并且用代码写好封存起来。每一次只要一个 command 所有测试都会重跑一遍。

参考

YouTube – Jest Crash Course Jest 基本用法

YouTube – Why Vitest Is Better Than Jest Vitest 取代 Jest

Unit Test vs e2e Test

unit test 单元测试指的是很小单元的测试。比如测试一个方法。

用人工测试的话,它的过程是调用方法,console 看看 result。

e2e (end-to-end) 测试的是 final UI。比如一个 login 过程。

用人工测试的话,它的过程是进入页面,填 form,submit。查看结果。

单元测试是我们在写代码时做的小测试。e2e 则是我们完成了一个小组件/页面后对整体的一个最终测试。

傻傻分不清的测试工具

Jasmine 是最古老的单测代码。

Karma 也很古老,它是运行 Jasmine 的环境 (Angular2 默认就是 Jasmine + Karma 做单元测试,但最近 Karma 已经被淘汰了)

Jest 的单测代码和 Jasmine 雷同,同时它具备 Karma 运行环境的能力。所以单单一个 Jest 就可以完成单元测试了。它是 Facebook 出品,在 React 生态很火的。

Vitest 和 Jest 雷同,单侧代码也和 Jasmine 雷同,也具备运行环境。所以它可以完全替代 Jest。它是 Vue 生态,基于 Vite 的单侧工具。

jsdom 一般上,Jest 和 Vitest 都是用 Node.js 测试 JavaScript。如果涉及到 DOM、BOM 需要搭配 jsdom。它和 Jest 是同一个生态的,但 Vitest 也是可以用它。

Protractor 是 e2e 测试工具。Angular2 默认用它,但目前已经被淘汰了。

Cypress 是 e2e 测试工具,可以用来替代 Protractor。

Mocha 类似 Jest、Vitest。但比较轻,不完善。Cypress 是基于 Mocha 的哦。

Jasmine vs Jest vs Vitest

Jasmine 比较古老一些。以前我写 Angular 时也玩过它。虽然它古老,但也不过时,因为所有的测试工具使用方式和语法都大同小异。你会了一个再学另一个只是分分钟的事情。

题外话:目前 Angular 默认使用 Jasmine + Karma 做测试,v16 后也支持 Jest + jsdom,而随着 Karma 被丢弃了,未来默认会改成 Jasmine + Web Test Runner,想了解更多可以看这两篇。

Angular Testing in 2023 - Past, Present, and Future

Docs – Moving Angular CLI to Jest and Web Test Runner

Jest 是 Facebook 出的,伴随 React,所以它目前是最受欢迎的。但它有一个缺点,那就是对 ES Module 支持还不够完善。Github – Meta: Native support for ES Modules

Vitest 则属于 Vue、Vite 生态。比较新,非常适合搭配 Vite 来使用。

选择

没什么好选的,你用 React 就用 Jest,用 Vue 就用 Vitest,用 Angular 就 Jasmine。语法都差不多的。你看一下 Jest migrate to Vitest 就体会到了。

Get Started

follow Vite 教程 创建一个项目。

安装 Vitest

yarn add vitest --dev

module & test module

创建 core.ts 和 core.test.ts

core.ts 是我们的逻辑代码,core.test.ts 是我们的测试代码。测试文件的命名规范是加一个 '.test' 在中间。

JavaScript 一个 file 等于一个 module,通常我们是 1 个 module file 对应一个 test file。

core.ts

function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}

我们写一个简单的 sum 方法,功能是把所有传入的数目累加起来。

core.test.ts

如果没有使用测试工具,我们大概会这样测试。

写一段

const value = sum(1, 2, 3);
console.log(value); // 6

然后用游览器或 Node.js 运行一下,看看 log 出来的结果是不是 6,是的话就 ok 了,测试代码可以删了,happy ending。

直到某天发现 sum 的实现有问题 (maybe 性能),我们修改了 sum 的代码,然后呢,我们就得再写一遍测试代码,再运行看看结果。这就是一个重复性的低能工作。

那使用测试工具呢?

core.test.ts 代码长这样

import { describe, it, expect } from "vitest";
import { sum } from "./core"; describe(`test 'sum' function`, () => {
it("should return sum of numbers", () => {
const value = sum(1, 2, 3);
expect(value).toBe(6);
}); it("should return zero when no passing any numbers", () => {
const value = sum();
expect(value).toBe(0);
});
});

我们一个一个看

describe 是我们要测试的单元,这个颗粒度是我们自己决定的,例子中就是测试 sum 这个方法。(注: describe 允许嵌套。所以我们可以任意去 grouping 分组)

it 是我们要测试的 condition。比如同一个方法,我们要测试多种不同的参数调用。那么每一个 it 就表示一种不同的 "情况"

expect 是我们期望的结果。在每一个 it 中,我们可能有多个 expect,上面例子比较简单,所以只有一个 expect,下面会有更复杂的例子。

测试代码写好后,我们运行 command

yarn vitest

效果

绿色勾勾表示测试成功!

假如,sum 的代码写得不对。

运行测试的结果

各种错误信息。

至此,我们成功把测试封装了起来,以后即便修改 sum 的代码,我们也不需要重复写测试咯~

expect

expect 跟随着许多验证的方式

toBe

它是 Object.is

expect("1").toBe("1"); // pass
expect("1").toBe(1); // fail
expect({}).toBe({}); // fail
expect([]).toBe([]); // fail
expect(NaN).toBe(NaN); // pass
expect(0).toBe(-0); // fail

toEqual

equal 适合用于 array 和 object 的对比,不看指针,看值。

expect([1, 2, 3]).toEqual([1, 2, 3]); // pass
expect({ values: [1, 2, 3] }).toEqual({ values: [1, 2, 3] }); // pass
expect(1).toEqual("1"); // fail

它只是把引用类型当值类型对比,可没有自动转换类型哦。

toBeTruthy & toBeFalsy

expect(true).toBeTruthy(); // pass
expect(1).toBeTruthy(); // pass
expect("0").toBeTruthy(); // pass
expect([]).toBeTruthy(); // pass
expect({}).toBeTruthy(); // pass expect(false).toBeFalsy(); // pass
expect(undefined).toBeFalsy(); // pass
expect(null).toBeFalsy(); // pass
expect("").toBeFalsy(); // pass
expect(0).toBeFalsy(); // pass

它会自动转换类型,类似于 if (value)

toBeNull、toBeUndefined、toBeNaN

expect(NaN).toBeNaN();
expect(null).toBeNull();
expect(undefined).toBeUndefined();

它没有 toBeNullOrUndefined 哦。可以这样实现

expect(null == null).toBeTruthy();
expect(undefined == null).toBeTruthy();

only、skip

it 和 describe 都有 .only 和 .skip 方法。

it.only("test1", () => {
expect(1).toBe(1);
}); it("test2", () => {
expect(1).toBe(1);
});

这个 file 只有 it.only 会执行测试。

it.skip("test1", () => {
expect(1).toBe(1);
}); it("test2", () => {
expect(1).toBe(1);
}); it("test3", () => {
expect(1).toBe(1);
});

skip 表示不执行这个测试,其它的都执行。

before、after

beforeEach、afterEach 会在每一个 it 执行前触发,我们可以用来做初始化,或者 reset shared variable 等等。

describe(`test 'sum' function`, () => {
let sharedValue = 0;
beforeEach(() => {
sharedValue = 0;
});
afterEach(() => {
sharedValue = 0;
}); it("should return sum of numbers", () => {
const value = sum(1, 2, 3);
expect(value).toBe(6);
}); it("should return zero when no passing any numbers", () => {
const value = sum();
expect(value).toBe(0);
});
});

beforeAll、afterAll 则只会触发一次。

Mock

mock 是模仿/模拟的意思。模仿什么呢?函数。我们看例子体会。

mock callback function for test caller

我们有一个 forEach 的函数,它接收一个 callback function,在 looping 时把当前 value 和 index 传入到 callback 中

export function forEach<T>(
values: T[],
callbackfn: (value: T, index: number) => void
): void {
for (let index = 0; index < values.length; index++) {
const value = values[index];
callbackfn(value, index);
}
}

我们要测试 callback funcion 是否正确被调用。

describe(`test 'forEach' function`, () => {
it("should call callbackfn with correct parameters", () => {
var values = ["a", "b", "c"]; const callbackfn = vi.fn(); // mock 一个 callback function,它算是一个 proxy 函数,它除了可以调用之外,还有 tracking 信息 forEach(values, callbackfn); // 执行 forEach,执行完后,我们的 callbackfn 就有了调用的数据 expect(callbackfn).toBeCalledTimes(3); // 查看是否被调用了 3 次
expect(callbackfn).nthCalledWith(1, "a", 0); // 第一次调用时,传入的 parameters 是否是 'a', 0
expect(callbackfn).nthCalledWith(2, "b", 1);
expect(callbackfn).nthCalledWith(3, "c", 2); // mock 有许多数据可以用,上面都是一些封装好,常用到的验证,如果我们要更多的验证可以直接访问 mock 内的数据
// 等价于 .toBeCalledTimes(3)
expect(callbackfn.mock.calls.length).toBe(3);
// 等价于 .nthCalledWith(1, "a", 0)
expect(callbackfn.mock.calls[0][0]).toBe("a");
expect(callbackfn.mock.calls[0][1]).toBe(0);
});
});

mock function for cut off dependence(斩断依赖关系)

我们想测试函数 A,但函数 A 内部依赖了函数 B。假如函数 B 的代码有问题,那将导致函数 A 也测试失败。这样就很乱。所以要斩断依赖。

单元测试就要确保测试足够 "单元",不可以有依赖,怎么斩断依赖呢?答案是 mock 函数 B。

export function generateNextNumber(random: () => number): number {
return random() + 1;
}

generateNextNumber 依赖了一个 random 函数。

const random = vi.fn(); // mock random
random.mockReturnValue(10); // 配置 random 将返回 number 10
const value = generateNextNumber(random); // 调用 generateNextNumber
expect(value).toBe(11); // 最终 10 + 1 = 11

通过 mockReturnValueOnce 我们还可以指定每一次返回值都不一样。

const random = vi.fn();
random.mockReturnValueOnce(10).mockReturnValueOnce(20);
expect(random()).toBe(10);
expect(random()).toBe(20);

mock module by spyOn

上面 random 函数是通过参数传进去的,如果它不是参数而是 import from another module 我们如何 mock 呢?

这时需要 spyOn

import { it, expect, vi } from "vitest";
import { generateNextNumber } from "./core";
import * as shared from "./shared"; const random = vi.spyOn(shared, "random");
random.mockReturnValue(10); it("test", () => {
expect(generateNextNumber()).toBe(11);
});

spyOn 可以 mock 一个对象中的方法。

mock inner function

参考: Stack Overflow – Jest mock inner function

在同一个 file 里,函数间有依赖。这个不能直接 spyOn mock。因为 funcA 直接调用 funcB 的。

解决方法有 2 个,第一个是把 funcB 移出去另一个 file。

第二个是不要直接调用 funcB,改成 import self 调用

这样就可以 spyOn mock 了。

mock localStorage

Vitest by default 的环境是 Node.js。如果我们代码有用到 DOM、BOM 的话需要借助 jsdom

yarn add jsdom --dev

vite.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'jsdom',
},
});

它和 Vite 公用同一个 config file,但 import 改成 vitest/config。.

下面这个函数依赖了 localStorage

export function generateNextNumber(): number {
const currentNumber = +localStorage.getItem("currentNumber")!;
return currentNumber + 1;
}

测试代码

import { it, expect, vi } from "vitest";
import { generateNextNumber } from "./core"; const getItem = vi.spyOn(Storage.prototype, "getItem");
getItem.mockReturnValue("10"); it("test", () => {
expect(generateNextNumber()).toBe(11);
});

Vitest UI

如果看不惯 command line 的 test message。Vitest 还有 UI 版本哦。

安装

yarn add @vitest/ui --dev

在启动 command 加上 --ui 就可以了

vitest --ui

效果

Debug Mode

在 test file 打断点。

开启 JavaScript Debug Terminal

运行 yarn vitest 就可以了

效果

工具 – Vitest 与单元测试的更多相关文章

  1. Java开发工具IntelliJ IDEA单元测试和代码覆盖率图解

    原文 http://www.cnblogs.com/xiongmaopanda/p/3314660.html Java开发工具IntelliJ IDEA使用教程:单元测试和代码覆盖率 本文将展示如何使 ...

  2. 测试工具使用-Qunit单元测试使用过程

    031302620 应课程要求写一篇单元测试工具的博客,但是暂时没用到java,所以不想使用junit(对各种类都不熟悉的也不好谈什么测试),原计划是要用phpunit,但是安装经历了三个小时,查阅各 ...

  3. javaWeb开发小工具---MailUtils及其单元测试

    本次介绍的是,在javaWeb开发中,我们不免会遇到发送邮件的需求,比如:用户注册账号,需要激活登录,以及服务器定期向会员发送礼品信息等.所以参考有关资料,写了这个MailUtils工具类. 1.Ma ...

  4. JDBC小工具--TxQueryRunner及其单元测试

    1.TxQueryRunner的简介(需要相关jar包的请留言) TxQueryRunner类是common-dbutils下QueryRunner的子类,是用来简化JDBC操作的,所以要导入comm ...

  5. 使用coverage工具统计python单元测试覆盖率

    Coverage简介 Coverage是一种用于统计Python代码覆盖率的工具,通过它可以检测测试代码对被测代码的覆盖率如何.Coverage支持分支覆盖率统计,可以生成HTML/XML报告. 官方 ...

  6. Java程序单元测试工具对比——Parasoft Jtest与Junit

    Web应用程序开发中,面向对象的Java语言占了不少的比重.对于Java应用程序的测试方法或方式多种多样,比较典型的是程序员自己来完成程序测试中的一个部分——单元测试. 之前,慧都资讯提到单元测试是程 ...

  7. 单元测试系列:Mock工具之Mockito实战

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...

  8. 单元测试系列:Mock工具Jmockit使用介绍

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...

  9. 单元测试系列之五:Mock工具之Mockito实战

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...

  10. 单元测试系列之二:Mock工具Jmockit实战

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...

随机推荐

  1. django 如何查询汇总的求和时避免没有数据导致的错误

    django 如何查询汇总的求和时避免没有数据导致的错误 在 Django 中,如果你希望对某个字段进行求和操作,并在没有数据时返回默认值,可以使用 aggregate 结合 Coalesce 函数. ...

  2. [oeasy]python0074_设置高亮色_color_highlight_ansi_控制终端颜色

    更多颜色 回忆上次内容 上次我们搞的还是颜色 FG foreground 前景色 30-37 BG background 背景色 40-47 这些 都可以和字体样式 结合起来 难道 就这几种颜色 吗? ...

  3. 数据分析应该掌握的知识及SQL技能

    一.概念及常识 1.数据分析必备的统计学知识 描述统计学 1.平均值.中位数.众数 2.方差.标准差 3.统计分布:正态分布.指数分布.二项分布.卡方分布 推论统计学 1.假设检验 2.置信区间 3. ...

  4. JavaScript实现防抖函数

    什么是防抖?防抖就是避免快速多次点击后执行过多的函数调用,就是本来你点击支付宝支付后不小心在点击一次,导致支付函数被调用了两次,还都执行了,付了两次钱. 防抖函数的思想就是将函数延迟调用,延迟时间内不 ...

  5. 计算机网络中的检验和(checksum)(包括计算文件的检验和附有c++代码)

    介绍: 检验和(checksum),在数据处理和数据通信领域中,用于校验目的地一组数据项的和.它通常是以十六进制为数制表示的形式.如果校验和的数值超过十六进制的FF,也就是255. 就要求其补码作为校 ...

  6. linux一行执行多条命令 shell

    要实现在一行执行多条Linux命令,分三种情况: 1.&& 举例: lpr /tmp/t2 && rm /tmp/t2 第2条命令只有在第1条命令成功执行之后才执行.当 ...

  7. 【摘译+整理】System.IO.Ports.SerialPort使用注意

    远古的一篇博客,内容散落于博文和评论 https://sparxeng.com/blog/software/must-use-net-system-io-ports-serialport C# 和 . ...

  8. Redis的事务transactions与管道pipeline

    1.Redis的事务仅仅是保证事务里的操作会被连续独占的执行,Redis的命令执行是单线程, 2.Redis不保证事务的所有指令可以同时成功或者同时失败,只可以决定是否开始执行全部指令的能力,因此也没 ...

  9. Jmeter参数化5-JSON提取器

    后置处理器[JSON提取器] ,一般放于请求接口下面,用于获取接口返回数据里面的json参数值 1.以下json为例,接口返回的json结果有多组数据.我们要取出purOrderNo值 2.在jmet ...

  10. 【Java】Input,Output,Stream I/O流 03 系统标准流 & 打印流

    Standard Input,Output Stream 标准输入输出流 - System.in 系统标准输入流 所属InputStream Scanner(System.in); 默认从键盘获取输入 ...