工具 – Vitest 与单元测试

前言

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 就可以了

效果

 

posted @ 2023-08-01 13:57  兴杰  阅读(2314)  评论(0编辑  收藏  举报