前端单元测试库

# 前端单元测试框架

前端测试框架有

  • Mocha
  • Jasmine
  • Jest
  • Tape
  • Karma

# Mocha

安装

npm install mocha --save-dev
1

使用

const add = require("./add");
const assert = require("assert");

// describe:定义一组测试
describe("加法函数测试", function() {
    before(function() {
        // runs before all tests in this block
    });
    
    // it: 定义一个测试用例
    it("1 加 1 应该等于 2", function() {
        // assert: nodejs内置断言模块
        assert.equal(add(1, 1), 2);
    });
    
    after(function() {
        // runs after all test in this block
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

断言库

Mocha 支持should.js, chai, expect.js, better-assert, unexpected等断言库

//assert
assert.ok(add(1, 1));
assert.equal(add(1, 1), 2);
//shouldjs
(add(1, 1)).should.be.a.Number();
(add(1, 1)).should.equal(2);
//expectjs
expect(add(1, 1)).to.be.a("number");
expect(add(1, 1)).to.equal(2);
//chai支持should, expect, assert三种语法
1
2
3
4
5
6
7
8
9
10

should.jsexpect.js相较于assert语义性更强,且支持类型检测,而should.js在语法上更加简明,同时支持链式语法.and

  • chai.js断言库:接口丰富,文档齐全,可以对各种接口进行断言。
  • expect 库应用是非常广泛,拥有很好的链式结构和仿自然语言的方法。
  • 通常写同一个断言会有几个方法,比如:expect(response).to.be(true) 和 expect(response).equal(true)。

expect和should是BDD风格的,二者使用相同的链式语言来组织断言,但不同在于他们初始化断言的方式:expect使用构造函数来创建断言对象实例,而should通过为Object.prototype新增方法来实现断言(所以should不支持IE);expect直接指向chai.expect,而should则是chai.should()。

expect断言风格

  • ok :检查是否为真
  • true:检查对象是否为真
  • to.be、to:作为连接两个方法的链式方法
  • not:链接一个否定的断言,如 expect(false).not.to.be(true)
  • a/an:检查类型(也适用于数组类型)
  • include/contain:检查数组或字符串是否包含某个元素
  • below/above:检查是否大于或者小于某个限定值

assert风格是三种断言风格中唯一不支持链式调用的,Chai提供的assert风格的断言和node.js包含的assert模块非常相似。

Mocha 支持4种 hook,包括before / after / beforeEach / afterEach

Mocha 默认每个测试用例执行2000ms,超出时长则报错,所以在测试代码中如果有异步操作,则需要通过done函数来明确测试用例结束。done接受Error参数。

Mocha 在node环境下运行时,不支持 BOM 和 DOM 接口,需要引入jsdomjsdom-global库。

Mocha命令行基本用法:

  • mocha:默认运行test子目录里面的测试脚本,不包括子文件
  • mocha add.test.js:当前目录下面的该测试脚本。
  • mocha file1 file2 file3 : mocha命令后面紧跟测试脚本的路径和文件名,可以指定多个测试用例。

通配符:

  • mocha spec/{my,awesome}.js
  • mocha test/unit/*.js

生成格式

  • mocha –reporter spec:默认为spec格式,可设置其他格式。
  • mocha –recursive -R markdown > spec.md 。

网页查看

npm install –save-dev mochawesome

在gulp中运行mocha

安装gulp-mocha插件

npm install gulp-mocha --save-dev
1

gulpfile

gulp.task('mocha',function() {
	return 
})
1
2
3

# Jasmine

Jasmine 是一个功能全面的测试框架,内置断言expect;但是有全局声明,且需要配置,相对来说使用更复杂、不够灵活。

npm install jasmine --save-dev
1

Jasmine 的语法与 Mocha 非常相似,不过断言采用内置的expect()

# Jest

Jest 是一个功能全面的“零配置”测试框架,既集成了各种工具,且无需配置即可使用。

npm install --save-dev jest
1

Jest 中以test定义一个测试用例,且自带断言expect,断言库功能强大,但语法相较于should.js来说更复杂。

普通匹配:toBe, not.toBe

空匹配:toBeNull, toBeUndefined, toBeDefine, toBeTruthy, toBeFalsy

数字大小:toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toEqual, toBeCloseTo(用于浮点数)

正则匹配:toMatch

数组查询:toContain

构造匹配:toEqual(expect.any(constructor))

Jest 同样有四个hook,beforeAll/beforeEach/afterAll/afterEach

Jest 内置对 DOM 和 BOM 接口的支持。

Jest 内置覆盖统计,为了更方便地进行相关配置,我们可以创建一个配置文件jest.config.js

然后将package.json中的命名修改一下:"test-jest": "jest"

jest教程:http://github.yanhaixiang.com/jest-tutorial/#%E6%B5%8B%E8%AF%95%E9%9A%BE%E7%82%B9

写测试的难点在于:

不会配置。 Jest 的上手文档非常简单,甚至不需要配置。但真实情况是只要一个配置没配好,所有测试都跑不起来。测试不像开发,代码有问题可以慢慢调。 测试是一个 0 - 1 游戏,不是成功就是失败,挫败感非常强。

不知道要怎么 Mock。 这个绝对是经典中的经典。虽然官方文档有教程,但是真实的业务往往不是那么理想,远比文档要复杂的多。

不会构造测试用例。 刚接触测试时,很容易把做业务那套 “实现 XXX 功能” 的想法代入测试。但测试的重点不在于实现功能,而是构造用例。

没有测试策略。 上面是 “技” 的难点,测试还有 “术” 的难点。闷着头一通肝测试代码并不高效,使用合适的测试策略远比写 10 个测试用例重要。

好的测试会让你获得很高的代码信心,而不好的测试则会严重拖垮项目开发。所以,大家所厌恶的不应该是测试本身,而是那些维护性差的测试。

# 测试环境

在很多时候,我们前端的代码往往只在浏览器里运行,经常要用到浏览器的 API。由于 Jest 的测试文件也是 Node.js 环境下执行的,jest 提供了 testEnvironment 配置,添加 jsdom 测试环境后,全局会自动拥有完整的浏览器标准 API。原理是使用了 jsdom (opens new window) (opens new window)。 这个库用 JS 实现了一套 Node.js 环境下的 Web 标准 API。

module.exports = {
  testEnvironment: "jsdom",
}
1
2
3

# 支持ts

安装jest的ts转译器

npm i -D ts-jest@27.1.4
1

jest.config.js 里添加一行配置

module.exports = {
  preset: 'ts-jest',
  // ...
};
1
2
3
4

在安装jest的类型文件

npm i -D @types/jest@27.4.1
1

然后在 tsconfig.json 里加上 jestnode 类型声明:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}
1
2
3
4
5

也可以选择使用 babel-jest 来做转译,不过Babel 做转译的 缺点是无法让 Jest 在运行时做类型检查,所以更推荐大家使用 ts-jest,利用 tsc 来转译 TypeScript。

# React

测试react需要引入testing-library/react库

npm i -D @testing-library/react@12.1.4
1

# 快照测试

组件是有 HTML 结构的。 如果不对比一下 HTML 结构,很难说服自己组件没问题。但是这就引来了一个问题了:要怎么对比 HTML 结构?

最简单的方法就是把这个组件的 HTML 打印出来,拷贝到一个 xxx.txt 文件里,然后在下次跑用例时,把当前组件的 HTML 字符串和 xxx.txt 文件里的内容对比一下就知道哪里有被修改过。 这就是快照测试的基本理念,即:先保存一份副本文件,下次测试时把当前输出和上次副本文件对比就知道此次重构是否破坏了某些东西。

只不过 jest 的快照测试提供了更高级的功能:

  1. 自动创建把输出内容写到 .snap 快照文件,下次测试时可以自动对比
  2. 输出格式化的快照文件,阅读友好,开发者更容易看懂
  3. 当在做 diff 对比时,jest 能高亮差异点,而且对比信息更容易阅读

快照测试通过说明渲染组件没有变,如果不通过则有两种可能:

  1. 代码有 Bug。 本来好好的,被你这么一改,改出了问题
  2. 实现了新功能。 新功能可能会改变原有的 DOM 结构,所以你要用 jest --updateSnapshot 来更新快照

在title.test.tsx中添加一个快照测试

// tests/components/Title.test.tsx
import React from "react";
import { render } from "@testing-library/react";
import Title from "components/Title";

describe("Title", () => {
  it("可以正确渲染大字", () => {
    const { baseElement } = render(<Title type="large" title="大字" />);
    expect(baseElement).toMatchSnapshot();
  });

  it("可以正确渲染小字", () => {
    const { baseElement } = render(<Title type="small" title="小字" />);
    expect(baseElement).toMatchSnapshot();
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行测试后,会发现在 tests/components/ 下多了一个 Title.test.tsx.snap 文件

// tests/components/Title.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Title 可以正确渲染大字 1`] = `
<body>
  <div>
    <p
      style="font-size: 2em; color: red;"
    >
      大字
    </p>
  </div>
</body>
`;

exports[`Title 可以正确渲染小字 1`] = `
<body>
  <div>
    <p
      style="font-size: 0.5em; color: green;"
    >
      小字
    </p>
  </div>
</body>
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

快照测试的注意事项

避免大快照

现在 Title 比较简单,所以看起来还可以,但真实业务组件中动辄就有十几个标签,还带上很多乱七八糟的属性,生成的快照文件会变得无比巨大。

对于这个问题,我们能做的就是避免大快照,不要无脑地记录整个组件的快照,特别是有别的 UI 组件参与其中的时候

所以,对于那种输出很复杂,而且不方便用 expect 做断言时,快照测试才算是一个好方法。 这也是为什么组件 DOM 结构适合做快照,因为 DOM 结构有大量的大于、小于、引号这些字符。如果都用 expect 来断言,expect 的结果会写得非常痛苦。 不过,需要注意的是:不要把无关的 DOM 也记录到快照里,这无法让人看懂。

假错误

假如现在把 title 的 “大字” 改成 “我是一个大帅哥,马上就得到一个渲染报错

这里只是文案改了一下,业务代码并没有任何问题,测试却出错了,这就是测试中的 “假错误”。 虽然普通的单测、集成测试里也可能出现 “假错误”, 但是快照测试出现 “假错误” 的概率会更高,这也很多人不信任快照测试的主要原因。

在一些大快照,复杂组件的情况下,只要别的开发者改了某个地方,很容易导致一大片快照报错,基于人性的弱点,他们是没耐心看测试失败的原因的, 再加上更新快照的成本很低,只要加个 --updateSnapshot 就可以了,所以人们在面对快照测试不通过时,往往选择更新快照而不去思考 DOM 结构是否真的变了

这些因素造成的最终结果就是:不再信任快照测试。 所以,你也会发现市面上很多前端测试的总结以及文章都很少做 快照测试。很大原因是快照测试本身比较脆弱, 而且容易造成 “假错误”

总结出快照测试的适用场景:

  • 组件 DOM 结构的对比
  • 在线上跑了很久的老项目
  • 大块数据结果的对比

# 组件测试

编写组件测试文件

// tests/components/AuthButton/simple.test.tsx
import { render, screen } from "@testing-library/react";
import AuthButton from "components/AuthButton";
import React from "react";

describe('AuthButton', () => {
  it('可以正常展示', () => {
    render(<AuthButton>登录</AuthButton>)

    expect(screen.getByText('登录')).toBeDefined();
  });
})
1
2
3
4
5
6
7
8
9
10
11
12

直接这样写jest会报错

Jest 不会转译任何内容,因此我们一直用 tsc 来转译 TypeScript。由于 tsc 看不懂引入的 .less,导致了 Unexpected Token 报错

比较推荐的方法是把 .less 转译成空文件

除了 .less 文件,我们还要对非 JS 静态资源做转译,比如 jpg, svg, png 等等(这些不会影响测试)。

npm i -D jest-transform-stub@2.0.0
1

添加转译配置

// jest.config.js
module.exports = {
  // ...
  transform: {
    ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
  } 
}
1
2
3
4
5
6
7

# redux

# react hooks

测试hooks时不能像纯函数一样直接在测试文件中使用hooks,因为react只允许在组件顶层使用hooks

引入@testing-library/react-hooks

npm i -D @testing-library/react-hooks@8.0.0
1

使用

// tests/hooks/useCounter/renderHook.test.ts
import { renderHook } from "@testing-library/react-hooks";
import useCounter from "hooks/useCounter";
import { act } from "@testing-library/react";

describe("useCounter", () => {
  it("可以做加法", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(1);
    });

    expect(result.current[0]).toEqual(1);
  });

  it("可以做减法", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].dec(1);
    });

    expect(result.current[0]).toEqual(-1);
  });

  it("可以设置值", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(10);
    });

    expect(result.current[0]).toEqual(10);
  });

  it("可以重置值", () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current[1].inc(1);
      result.current[1].reset();
    });

    expect(result.current[0]).toEqual(0);
  });

  it("可以使用最大值", () => {
    const { result } = renderHook(() => useCounter(100, { max: 10 }));

    expect(result.current[0]).toEqual(10);
  });

  it("可以使用最小值", () => {
    const { result } = renderHook(() => useCounter(0, { min: 10 }));

    expect(result.current[0]).toEqual(10);
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# Enzyme

这是一个由 Airbnb 编写的包装库,它使得测试 React 组件变得更容易。同时,我们还要为我们使用的 React 不同版本安装适配器

Enzyme的API和jQuery操作DOM一样灵活易用,因为它使用的是cheerio库来解析虚拟DOM,而cheerio的目标则是做服务器端的jQuery。Enzyme兼容大多数断言库和测试框架,如chai、mocha、jasmine等。

安装

npm i --save-dev enzyme enzyme-adapter-react-16
1

enzyme支持三种方式的渲染:

shallow:浅渲染,是对官方的Shallow Renderer的封装。将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,因而效率非常高。不需要DOM环境, 并可以使用jQuery的方式访问组件的信息;

render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构。

mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期,用到了jsdom来模拟浏览器环境。

enzyme中有几个比较核心的函数需要注意,如下:

simulate(event, mock):用来模拟事件触发,event为事件名称,mock为一个event object;

instance():返回测试组件的实例;

find(selector):根据选择器查找节点,selector可以是CSS中的选择器,也可以是组件的构造函数,以及组件的display name等;

at(index):返回一个渲染过的对象;

get(index):返回一个react node,要测试它,需要重新渲染;

contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组;

text():返回当前组件的文本内容;

html(): 返回当前组件的HTML代码形式;

props():返回根组件的所有属性;

prop(key):返回根组件的指定属性;

state():返回根组件的状态;

setState(nextState):设置根组件的状态;

setProps(nextProps):设置根组件的属性;

利用enzyme渲染、操作dom,这样就可以配合Jest进行测试

import React from 'react'
import { mount } from 'enzyme'
import TodoItem from '../../src/components/TodoItem.jsx'

describe('待办事项-列表项组件', () => {
  test('渲染待办事项列表项', () => {
    const todo = {id: 2, title: '复习 React Hooks 使用', completed: false}
    const wrapper = mount(
      <TodoItem todo={todo} />
    )
    const p = wrapper.find('p')
    expect(p.text()).toBe('复习 React Hooks 使用')
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Sinon

# Vitest

Last Updated: 11/15/2022, 9:34:42 PM