首页 > 其他分享 >工具 – Vitest 与单元测试

工具 – Vitest 与单元测试

时间:2023-08-01 14:14:21浏览次数:42  
标签:Vitest sum random 单元测试 value toBe expect 工具 mock

前言

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

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

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

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

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

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

 

参考

YouTube – Jest Crash Course Jest 基本用法

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

 

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

效果

 

 

标签:Vitest,sum,random,单元测试,value,toBe,expect,工具,mock
From: https://www.cnblogs.com/keatkeat/p/17593475.html

相关文章

  • 你的下一个构建工具,何必是构建工具
    Yock是一个为构建而生的框架,他有点类似于nodejs和bazel,底层基于一个解释器封装了一些功能。正如nodejs那样,yock也基于封装过的lua实现了自己的包管理工具——ypm,这意味着引入第三方库成为可能。安装注意:无论是哪一种安装方式,下载完后都需要将yock挂载到本地环境中。在解压压缩......
  • (Python编程)目录工具
    ProgrammingPython,3rdEdition翻译最新版本见wiki:http://wiki.woodpecker.org.cn/moin/PP3eD欢迎参与翻译与修订。   4.3.DirectoryTools 4.3.目录工具Oneofthemorecommontasksintheshellutilitiesdomainisapplyinganoperationtoasetoffilesin......
  • calicoctl工具部署-k8s中的pod的方式
    1、概述 本文档介绍的是将calicoctl工具,以pod的方式,部署在k8s集群中2、部署过程 2.1下载calicoctl工具的镜像、上传到本地的镜像仓库中本次使用的版本是:calico/ctl:v3.21.4这个版本和当前运行的calico-node是一个版本(必须一致,包括后面的小版本) 拉取镜像dockerpul......
  • 工作可视化管理,工作流程管理-看板工具
    ​看板利用了对视觉内容的偏好,可以帮助团队理解和分析在工作中发生了什么,遇到了哪些问题和瓶颈,我们可以通过看板更好的可视化工作流程,可以在看板内自定义工作流程,首先创建工作流任务看板通过Leangoo领歌敏捷看板工具的轻量化协作项目模板,创建可视化工作流看板。Leangoo内置了......
  • vue 使用 eruda(移动端H5调试工具)
    <scriptsrc="https://cdn.bootcdn.net/ajax/libs/eruda/2.3.3/eruda.min.js"></script><script>eruda.init()</script> ......
  • MacOS苹果系统投屏工具—AirServer
    AirServer是一个Mac专用投屏工具,功能强大,并且可以通过网络和其他平台同步视频内容。可以使用多个设备进行投屏,快速查看同一局域网内的视频。支持的设备:苹果系统。支持Windows、Mac、Android、iOS、windows平台。→→↓↓载AirServer 1、支持Windows、Mac(部分苹果用......
  • QA|新版Pycharm如何关闭和开启自动补全功能?|Pycharm|工具相关
    自动补全开启状态: 自动补全关闭状态: 建议:新学者建议开启自动补全,这样可以把各个函数方法记忆的更加深刻! ......
  • 路由工具
    ACL列表ACL访问控制列表,即用于流量的匹配与控制,但也能够用于匹配路由条目。前缀列表与AC的区别1)ACL无法匹配路由掩码2)ACL无法匹配精确的路由如:存在两个路由192.168.1.0/24,192.168.1.0/16如果用ACL只匹配192.168.1.0/16的话应该这么写:[AR1]acl2000[AR1-acl-basic-2000]rulepermit......
  • android 单元测试之 JUnit
    android里面做单元测试第一,JUnit。     实用范围:     东西,比如业务逻辑,数据封装,数值计算等等。并不能测试androidapi。第二,采用Instrumentation.Android单元测试的主入口是InstrumentationTestRunner。它相当于JUnit当中TestRunner的作用。你可以将Instrumentat......
  • node js版本管理工具---NVM
    一、前言nvm(NodeVersionManager)是一个node的版本管理工具,可以快捷的进行node版本的安装、切换、卸载、查看等。它能够在项目开发中根据不同需求轻松切换所依赖不同版本的Node.js,从而让开发者可以在不同的环境之间进行切换,从而更好地保证软件的稳定性运行。二、安装1、linux或......