玩命加载中 . . .

前端单元测试框架——jest


学习 Jest 时,首先阅读官方文档,大致浏览下 API ,然后再去看一些大牛的博客或教程,会让学习的效果最大化。同时可以自己在本地起一个服务,实践学习到的知识。

下面我们先来看如何搭建服务,方便实践代码。

搭建项目

初始化

# 创建项目
mkdir study-jest
cd study-jest

# 初始化
npm init -y

# 安装依赖
npm i -D jest@27.5.1

目前 Jest 已经来到 29 版本了,但是 Jest@28 和 react@18 以及 @testing-library/react 一起使用时会有冲突,建议使用 稳定 的版本。

安装 Jest 后,用 jest-cli 初始化 jest 配置文件:

npx jest --init

可以按照图中的配置走,如果要使用 ts 的话也可以,后面会讲解如何让 Jest x TypeScript “合作”。

image.png

修改 package.json 中的 scripts 为:

{
    "scripts": {
        "test": "jest --watchAll", // 类似热更新,这样就不用频繁执行命令了
        "coverage": "jest --coverage" // 生成 web 页面文件
    }
}

支持 ts

如果在项目中使用 TypeScript ,由于 Jest 本身不做代码转译工作 ,那么我们在编写测试时需要调用已有的 转译器/编译器 来做代码转译,常见的转译器有 Babel 以及 TSC

这里默认读者已经在项目中使用了 TypeScript ,下面直接讲解转译的配置步骤:

  1. 安装 ts-jest
npm i -D ts-jest@27.1.4

注意,这里 ts-jest 一定要和 jest 的大版本一致!  比如 27 对 27,或者 26 对 26,否则会有兼容问题!

  1. 在 jest.config.js 里添加一行配置:
module.exports = {
  preset: 'ts-jest',
  // ...
};
  1. 安装对应的 Jest 类型声明包:
npm i -D @types/jest@27.4.1

同样地,TS 声明类型包的大版本最好和 jest 一样。

  1. 在 tsconfig.json 里加上 jest 和 node 类型声明:
{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}
  1. 最后执行 npm run test,测试通过。

路径别名

在引入其他文件的变量或函数时,我们可能需要写很长一句路径,是不是可以优化一下呢?

要实现这样的效果,我们可以在 jest.config.js 中的 moduleDirectories 添加 "src"

// jest.config.js
module.exports = {
  moduleDirectories: ["node_modules", "src"],
  // ...
}

显而易见的,这是写给 Jest 看的,别忘了还有转译器 TSC ,在 tsconfig.json 里指定 baseUrl 和 paths 路径:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "utils/*": ["src/utils/*"]
    }
  }
}

值得一提的是,所谓的 “路径简写” 本质上只是路径映射。所以 tsconfig.json 里的 paths 就是把 utils/xxx 映射成 src/utils/xxx, 而 jest.config.js 里的 moduleDirectories 则是把 utils/sum 当作第三方模块,先在 node_modules 里找,找不到再从 src/xxx 下去找。 所以这两者是有区别的。

支持 import 和 ES6 语法

安装 babel 核心包:

npm i @babel/core@7.4.5 @babel/preset-env -D

在项目根目录新建 .babelrc 文件,通过这个配置文件告诉 babel 应该如何将 ES6 转换成 ES5 :

{
    "presets": [
        [
            "@babel/preset-env", {
                "targets": {
                    "node": "current
                }
            }
        ]
    ]
}
  • 为什么安装了 babel 的核心包,然后写下配置文件,就可以达到转换语法的目的?
  • 这是因为 Jest 中有 babel-jest 组件,当我们使用 npm run test 时,它会先去检测根目录或者开发环境中是否安装了 babel 核心包,由于 package.json 中安装了 babel ,紧接着就会去找 .babelrc 文件,然后开始转化。

测试环境

很多时候,我们前端的代码需要在浏览器里运行,经常需要用到浏览器的 API ,例如 localStoragecookie 等,但是我们并不能直接这样测试,因为 Node.js 环境没有 localStorage ,所以会得到报错:localStorage is not defined

既然 Node.js 没有 localStorage ,那么我们想办法让运行环境支持使用浏览器的 API ,Jest 提供了这样的功能,即 testEnvironment 配置。

jest.config.js 中配置:

module.exports = {
    testEnvironment: "json"
}

添加了 jsdom 测试环境之后,全局会自动拥有完整的浏览器标准 API 。原理jsdom 这个库自己用 JS 实现了一套 Node.js 环境下的 Web 标准 API 。由于 Jest 的测试文件也是 Node.js 环境下执行的,所以 Jest 用这个库充当了浏览器环境的 Mock 实现。

我们可以写一段测试:

src/utils/storage.ts 写入 localStorage 的添加和删除:

const KEY_NAME = "my-app-";

const set = (key: string, value: string) => {
  localStorage.setItem(KEY_NAME + key, value);
};

const get = (key: string) => {
  return localStorage.getItem(KEY_NAME + key);
};

const storage = {
  get,
  set,
};

export default storage;

tests/utils/storage.test.ts 添加测试用例:

import storage from "utils/storage";

describe("storage", () => {
  it("可以缓存值", () => {
    storage.set("newKey", "hello");
    expect(localStorage.getItem("my-app-newKey")).toEqual("hello");
  });

  it("可以设置值", () => {
    localStorage.setItem("my-app-newKey", "hello");
    expect(storage.get("newKey")).toEqual("hello");
  });
});

执行 npm run test 可以看到测试是成功的

Jest 常见匹配器

  1. toEqual
test('toEqual 匹配器', () => {
    const a = { number: '1' }
    expect(a).toEqual({ number: '1' })
}
  1. toBeNull
test('toBeNull 匹配器', () => {
    const a = null
    expect(a).toBeNull()
}
  1. toBeUndefined
test('toBeUndefined 匹配器', () => {
    const a = toBeUndefined
    expect(a).toBeUndefined()
}
  1. toBeDefined
test('toBeDefined 匹配器', () => {
    const a = 'foursheep' // 任意定义了变量,就能通过测试
    expect(a).toBeDefined()
}
  1. toBeTruthy
test('toBeTruthy 匹配器', () => {
    const a = 1 // 这个值返回 true ,就能通过测试
    expect(a).toBeTruthy()
}
  1. toBeFalsy
test('toBeFalsy 匹配器', () => {
    const a = 0 // 这个值返回 false ,就能通过测试
    expect(a).toBeFalsy()
}
  1. toBeFalsy
test('toBeFalsy 匹配器', () => {
    const a = 0 // 这个值返回 false ,就能通过测试
    expect(a).toBeFalsy()
}
  1. toBeGreaterThan

类似于 >

test('toBeGreaterThan 匹配器', () => {
    const count = 10
    expect(count).toBeGreaterThan(9)
}
  1. toBeLessThan

类似于 <

test('toBeLessThan 匹配器', () => {
    const count = 10
    expect(count).toBeLessThan(11)
}
  1. toBeGreaterThanOrEqual

类似于 >=

test('toBeGreaterThanOrEqual 匹配器', () => {
    const count = 10
    expect(count).toBeGreaterThanOrEqual(10)
}
  1. toBeLessThanOrEqual

类似于 <=

test('toBeLessThanOrEqual 匹配器', () => {
    const count = 10
    expect(count).toBeLessThanOrEqual(11)
}
  1. toBeCloseTo
test('toBeCloseTo 匹配器', () => {
    const one = 0.1
    const two = 0.2
    expect(one + two).toBeCloseTo(0.3)
}
  1. toMatch

匹配字符串

test('toMatch 匹配器', () => {
    const str = '1, 2, 3'
    expect(str).toMatch(4)
}
  1. toContain

匹配数组

test('toContain 匹配器', () => {
    const arr = ['1', '2', '3']
    const setArr = new set(arr)
    expect(arr).toContain('4')
    expect(setArr).toContain('4')
}
  1. toThrow

抛出异常才可以通过测试

const throwNewErrorFunc = function () {
    throw new Error ('this is Error')
}

test('toThrow 匹配器', () => {
    expect(throwNewErrorFunc).not.toThrow()
}

Jest 官网匹配器

Jest 中的钩子函数

四个钩子

  1. beforeAll

所有 测试用例执行 之前 要执行一次这个钩子函数

  1. beforeEach

每一个 测试用例执行 之前 都要执行这个钩子函数

  1. afterAll

所有 测试用例执行 之后 要执行一次这个钩子函数

  1. afterEach

每一个 测试用例执行 之后 都要执行这个钩子函数

钩子函数的作用域

  • 钩子函数在父级分组可作用于子集,类似于继承
  • 钩子函数同级分组作用域是互不干扰的,各其作用
  • 先执行外部的钩子函数,再执行内部的钩子函数

文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录