测试JEST相关

356 阅读12分钟

各框架特点

Jest

  • facebook 坐庄
  • 基于 Jasmine 至今已经做了大量修改添加了很多特性
  • 开箱即用配置少,API简单
  • 支持断言和仿真
  • 支持快照测试
  • 在隔离环境下测试
  • 互动模式选择要测试的模块
  • 优雅的测试覆盖率报告,基于Istanbul
  • 智能并行测试(参考)
  • 全局环境,比如 describe 不需要引入直接用
  • 较多用于 React 项目(但广泛支持各种项目)

Mocha

  • 灵活(不包括断言和仿真,自己选对应工具)
    流行的选择:chai,sinon
  • 社区成熟用的人多,测试各种东西社区都有示例
  • 需要较多配置
  • 可以使用快照测试,但依然需要额外配置

Jasmine

  • 开箱即用(支持断言和仿真)
  • 全局环境
  • 比较'老',坑基本都有人踩过了

AVA

  • 异步,性能好
  • 简约,清晰
  • 快照测试和断言需要三方支持

Tape

  • 体积最小,只提供最关键的东西
  • 对比其他框架,只提供最底层的 API

JEST

中文官网:jestjs.io/docs/code-t…

1 什么是 Jest

Jest 是 Facebook 的一套开源的 JavaScript 测试框架,它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架。基于Jasmine的JavaScript单元测试框架。Jest源于Facebook的构想,用于快速、可靠地测试Web聊天应用。它吸引了公司内部的兴趣,Facebook的一名软件工程师Jeff Morrison半年前又重拾这个项目,改善它的性能,并将其开源。Jest的目标是减少开始测试一个项目所要花费的时间和认知负荷,因此它提供了大部分你需要的现成工具:快速的命令行接口、Mock工具集以及它的自动模块Mock系统。此外,如果你在寻找隔离工具例如Mock库,分其它工具大部将让你在测试中(甚至经常在你的主代码中)写一些不尽如人意的样板代码,以使其生效。Jest与Jasmine框架的区别是在后者之上增加了一些层。最值得注意的是,运行测试时,Jest会自动模拟依赖。Jest自动为每个依赖的模块生成Mock,并默认提供这些Mock,这样就可以很容易地隔离模块的依赖。

Jest支持Babel,我们将很轻松的使用ES6的高级语法

Jest支持webpack,非常方便的使用它来管理我们的项目

Jest支持TypeScript,书写测试用例更加严谨

  1. 简化API

    Jest既简单又强大,内置支持以下功能:

    • 灵活的配置:比如,可以用文件名通配符来检测测试文件。
    • 测试的事前步骤(Setup)和事后步骤(Teardown),同时也包括测试范围。
    • 匹配表达式(Matchers):能使用期望expect句法来验证不同的内容。
    • 测试异步代码:支持承诺(promise)数据类型和异步等待async / await功能。
    • 模拟函数:可以修改或监查某个函数的行为。
    • 手动模拟:测试代码时可以忽略模块的依存关系。
    • 虚拟计时:帮助控制时间推移。
  2. 性能与隔离

    Jest文档里写道:

    Jest能运用所有的工作部分,并列运行测试,使性能最大化。终端上的信息经过缓冲,最后与测试结果一起打印出来。沙盒中生成的测试文件,以及自动全局状态在每个测试里都会得到重置,这样就不会出现两个测试冲突的情况。

    Mocha用一个进程运行所有的测试,和它比较起来,Jest则完全不同。要在测试之间模拟出隔离效果,我们必须要引入几个测试辅助函数来妥善管理清除工作。这种做法虽然不怎么理想,但99%的情况都可以用,因为测试是按顺序进行的。

  3. 沉浸式监控模式

    快速互动式监控模式可以监控到哪些测试文件有过改动,只运行与改动过的文件相关的测试,并且由于优化作用,能迅速放出监控信号。设置起来非常简单,而且还有一些别的选项,可以用文件名或测试名来过滤测试。我们用Mocha时也有监控模式,不过没有那么强大,要运行某个特定的测试文件夹或文件,就不得不自己创造解决方法,而这些功能Jest本身就已经提供了,不用花力气。

  4. 代码覆盖率&测试报告

    Jest内置有代码覆盖率报告功能,设置起来易如反掌。可以在整个项目范围里收集代码覆盖率信息,包括未经受测试的文件。

    要使完善Circle CI整合,只需要一个自定义报告功能。有了Jest,用jest-junit-reporter就可以做到,其用法和Mocha几乎相同。](github.com/michaelleea…)

  5. 快照功能

    快照测试的目的不是要替换现有的单元测试,而是要使之更有价值,让测试更轻松。在某些情况下,某些功能比如React组件功能,有了快照测试意味着无需再做单元测试,但同样这两者不是非此即彼。 

2 怎么用 Jest

安装

新建文件夹然后通过npm 命令安装:

 npm install --save-dev jest

 

或者通过yarn来安装:

 yarn add --dev jest
  

然后就可以开始测试了

也可用npm install -g jest进行全局安装;并在 package.json 中指定 test 脚本:

 {
 "scripts": {
 "test": "jest"
 }
}
  

Jest 的测试脚本名形如.test.js,不论 Jest 是全局运行还是通过npm test运行,它都会执行当前目录下所有的*.test.js 或 *.spec.js 文件、完成测试。

ES6语法支持:

  1. 安装依赖
 yarn add --dev babel-jest @babel/core @babel/preset-env
  
  1. 配置.babelrc
 {
 "presets": [
 [
 "@babel/preset-env",
 {
 "targets": {
 "node": "current"
 }
 }
 ]
 ]
}
  

接下来就可以使用ES6的语法了~~~

更多高阶的ES6/7/8…语法,可以参见:babel官网

关于Typescript的支持,可以参见 :Using Typescript

3 初识 Jest

4 有能力的Jest

4.1 匹配器

什么是匹配器,有什么用?

Jest uses "matchers" to let you test values in different ways(Jest 使用“匹配器”让您以不同方式测试值)

 

4.2 异步代码测试

1:callback方式

test('the data is peanut butter'done => { 
 function callback(data) { 
 try { 
 expect(data).toBe('peanut butter'); 
 done(); 
 } catch (error) { 
 done(error); 
 } 
 } 
 fetchData(callback); 
 });

2:Promises

test('the data is peanut butter'() => { 
 return fetchData().then(data => { 
 expect(data).toBe('peanut butter'); 
 }); 
 });

3:.resolves / .rejects

test('the data is peanut butter'() => { 
 return expect(fetchData()).resolves.toBe('peanut butter'); 
});

4:Async/Await

test('the data is peanut butter'async () => { 
 await expect(fetchData()).resolves.toBe('peanut butter'); 
 }); 
 test('the fetch fails with an error'async () => { 
 await expect(fetchData()).rejects.toMatch('error'); 
 });

4.3 mock函数和spy

 

Mock与Spy

 

mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。

Mock 是单元测试中经常使用的一种技术。单元测试,顾名思义测试的重点是某个具体单元。但是在实际代码中,代码与代码之间,模块与模块之间总是会存在着相互引用。这个时候,剥离出这种单元的依赖,让测试更加独立,使用到的技术就是 Mock。

 

  • jest.fn(implementation)  返回一个新的、未使用的模拟函数
  • jest.isMockFunction(fn)   确定给定的函数是否是模拟函数
  • jest.spyOn(object, methodName)   创建一个类似于jest.fn但也跟踪对 的调用的模拟函数
  • jest.spyOn(object, methodName, accessType?)  从 Jest 22.1.0+ 开始,该jest.spyOn方法采用可选的第三个参数accessType,可以是'get'或'set',这在你想分别监视 getter 或 setter 时被证明是有用的。
  • jest.clearAllMocks() 清除所有模拟的mock.calls,mock.instances和mock.results属性
  • jest.resetAllMocks()  重置所有模拟的状态
  • jest.restoreAllMocks()  将所有模拟恢复到其原始值

为什么要使用Mock函数?

在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。

Mock函数提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

举个例子:

 

 // math.js
export const getFooResult = () => {
 // foo logic here
};
export const getBarResult = () => {
 // bar logic here
};
// caculate.js
import { getFooResult, getBarResult } from "./math";
export const getFooBarResult = () => getFooResult() + getBarResult();
  

此时,getFooResult() 和 getBarResult() 就是 getFooBarResult 这个函数的依赖。如果我们关注的点是 getFooBarResult 这个函数,我们就应该把 getFooResult 和 getBarResult Mock 掉,剥离这种依赖。下面是一个使用 Jest 进行 Mock 的例子。

jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

 test('测试jest.fn()调用', () => {
 let mockFn = jest.fn();
 let result = mockFn(1, 2, 3);
 // 断言mockFn的执行后返回undefined
 expect(result).toBeUndefined();
 // 断言mockFn被调用
 expect(mockFn).toBeCalled();
 // 断言mockFn被调用了一次
 expect(mockFn).toBeCalledTimes(1);
 // 断言mockFn传入的参数为1, 2, 3
 expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
  

情景一:设置函数 的返回值

 // calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
 
test('getResult should return result getFooResult() + getBarResult()', () => {
 // mock add方法和multiple方法
 fooBar.getFooBarResult = jest.fn(() => 10);
 fooBar.getBarResult = jest.fn(() => 5);
 
 const result = getFooBarResult();
 
 expect(result).toEqual(15);
});
  

Mock其实就是一种Spies,在Jest中使用spies来“spy”(窥探)一个函数的行为。

Jest文档对于spies的解释](jestjs.io/docs/en/moc…):

Mock函数也称为“spies”,因为它们让你窥探一些由其他代码间接调用的函数的行为,而不仅仅是测试输出。你可以通过使用 jest.fn() 创建一个mock函数。

  简单来说,一个spy是另一个内置的能够记录对其调用细节的函数:调用它的次数,使用什么参数。

 // calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
 
test('getResult should return result getFooResult() + getBarResult()', () => {
 // mock add方法和multiple方法
 fooBar.getFooResult = jest.fn(() => 10);
 fooBar.getBarResult = jest.fn(() => 5);
 
 const result = getFooBarResult();
 
 // 监控getFooResult和getBarResult的调用情况.
 expect(fooBar.getFooResult).toHaveBeenCalled();
 expect(fooBar.getBarResult).toHaveBeenCalled();
});
  

情景二:捕获函数调用情况

 // bot method
const bot = {
 sayHello: name => {
 console.log(`Hello ${name}!`);
 }
};
// test.js
describe("bot", () => {
 it("should say hello", () => {
 const spy = jest.spyOn(bot, "sayHello");
 bot.sayHello("Michael");
 expect(spy).toHaveBeenCalledWith("Michael");
 spy.mockRestore();
 });
});
  

我们通过 jest.spyOn 创建了一个监听 bot 对象的 sayHello 方法的 spy。它就像间谍一样监听了所有对 bot#sayHello 方法的调用。由于创建 spy 时,Jest 实际上修改了 bot 对象的 sayHello 属性,所以在断言完成后,我们还要通过 mockRestore 来恢复 bot 对象原本的 sayHello方法。

 

Jest的spyOn介绍

情景三:修改函数的内容实现

 const bot = {
 sayHello: name => {
 console.log(`Hello ${name}!`);
 }
};
 
describe("bot", () => {
 it("should say hello", () => {
 const spy = jest.spyOn(bot, "sayHello").mockImplementation(name => {
 console.log(`Hello mix ${name}`)
 });
 
 bot.sayHello("Michael");
 
 expect(spy).toHaveBeenCalledWith("Michael");
 
 spy.mockRestore();
 });
});
  

使用spyOn方法,还可以去修改Math.random这样的函数 

jest.spyOn(Math, "random").mockImplementation(() => 0.9);

 

举个例子:

 // getNum.js
const arr = [1,2,3,4,5,6];
const getNum = index => {
 if (index) {
 return arr[index % 6];
 } else {
 return arr[Math.floor(Math.random() * 6)];
 }
};
 
// num.test.js
import { getNum } from '../src/getNum'
 
describe("getNum", () => {
 it("should select numbber based on index if provided", () => {
 expect(getNum(1)).toBe(2);
 });
 
 it("should select a random number based on Math.random if skuId not available", () => {
 const spy = jest.spyOn(Math, "random").mockImplementation(() => 0.9);
 expect(getNum()).toBe(6);
 expect(spy).toHaveBeenCalled();
 spy.mockRestore();
 });
});
  

4.4 定时器模拟

jest.useFakeTimers() or jest.useRealTimers()

4.5 DOM操作

'use strict';

jest.mock('../fetchCurrentUser');

test('displays a user after a click', () => {
// Set up our document body
document.body.innerHTML =
'<div>' +
' <span id="username" />' +
' <button id="button" />' +
'</div>';

// This module has a side-effect
require('../displayUser');

const $ = require('jquery');
const fetchCurrentUser = require('../fetchCurrentUser');

// Tell the fetchCurrentUser mock function to automatically invoke
// its callback with some data
fetchCurrentUser.mockImplementation(cb => {
cb({
fullName: 'Johnny Cash',
loggedIn: true,
});
});

// Use jquery to emulate a click on our button
$('#button').click();

// Assert that the fetchCurrentUser function was called, and that the
// #username span's inner text was updated as we'd expect it to.
expect(fetchCurrentUser).toBeCalled();
expect($('#username').text()).toEqual('Johnny Cash - Logged In');
});

 

@testing-library/react

www.npmjs.com/package/@te…

Simple and complete React DOM testing utilities that encourage good testing practices.

简单而完整的 React DOM 测试实用程序,支持良好的测试实践。

 

4.6 快照测试

5 标识美男Jest

  来个例子: 

创建一个react项目  npx create-react-app my-app

jest-demo.zipmy-jest.zip

资料来源:

jest中文官网:jestjs.io/docs/code-t…

@testing-library/react地址 :www.npmjs.com/package/@te…

jest教程:juejin.cn/post/684490…

使用 React Testing Library 和 Jest 完成单元测试:segmentfault.com/a/119000002…