Jest 基础学习

132 阅读9分钟

Jest 文档学习

文章内容主要来自官方文档的摘要以及个人理解

参考文章:

快速开始

首先需要安装Jest

npm install --save-dev jest
    或
yarn add --dev jest

运行npx jest --init生成配置文件;如果希望支持Typescript,可以安装以下依赖,运行结束后会自动生成配置文件

yarn add --dev ts-jest @types/jest
yarn ts-jest config:init

运行npm i babel-jest @babel/core @babel/preset-env -D安装babel,并且配置.babelrc如下

Would you like to use Typescript for the configuration file? ... yes

编写需要测试的代码(以下是两数之和的函数)

function sum(a, b) {  
return a + b;  
}  
module.exports = sum;

如果对这个函数写一个测试其正确是否的测试程序,它可能构思是这这样的:

期望 sum(1,2) 的结果是3

进一步转化为英文

expect sum(1,2) to be 3

用程序性的语言表示,expect作为一个函数,为它传入想要测试的对象(sum函数),把输出结果也做一层封装toBe(3)

expect(sum(1,2)).toBe(3)

更进一步,添加描述信息。我们可以再做一层封装,定义一个test函数,它有两个参数,第一个参数是一些描述性信息(这里是 测试sum函数),第二个参数是一个函数,函数里可以执行我们上面的逻辑,如下

test("1 + 2 = 3", () => {
    expect(sum(1,2)).toBe(3)
})

Matchers 匹配器

用于匹配测试程序结果的方法被称之为matcher,即在expect函数后面跟着的判断结果的toBe之类的方法

常见匹配器

toBe

toBe使用Object.is()来进行精准匹配

Object.is() 方法判断两个值是否为同一个值,如果满足以下任意条件则两个值相等:

  • 都是 undefined

  • 都是 null

  • 都是 true 或都是 false

  • 都是相同长度、相同字符、按相同顺序排列的字符串

  • 都是相同对象(意味着都是同一个对象的值引用)

  • 都是数字且

    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 都是同一个值,非零且都不是 NaN

toEqual toStrictEqual

检查对象的值,而不是引用地址

test('对象赋值', () => {  
    const data = {one: 1};  
    data['two'] = 2;  
    expect(data).toEqual({one: 1, two: 2});  
});

toEqual递归查找对象或数组的每个字段

提示 使用toStrictEqual优于使用toEqualtoEqual只是简单忽略undefined值,而toStictEqual则考虑它们

not

我们还可以使用与匹配相反的 not 来进行测试

test('adding positive numbers is not zero', () => {  
    for (let a = 1; a < 10; a++) {  
        for (let b = 1; b < 10; b++) {  
            expect(a + b).not.toBe(0);  
        }  
    }  
});

真值

代码中的undefinednull, and false有不同含义,若你在测试时不想区分他们,可以用真值判断。 Jest提供helpers供你使用。

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 与 toBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假

数字

test('two plus two', () => {
  const value = 2 + 2;
  // 大于
  expect(value).toBeGreaterThan(3);
  // 大于等于
  expect(value).toBeGreaterThanOrEqual(3.5);
  // 小于
  expect(value).toBeLessThan(5);
  // 小于等于
  expect(value).toBeLessThanOrEqual(4.5);
});

对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。

test('两个浮点数字相加', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});

字符串

您可以检查对具有 toMatch 正则表达式的字符串︰

test('there is no I in team', () => {  
    expect('team').not.toMatch(/I/);  
});  
  
test('but there is a "stop" in Christoph', () => {  
    expect('Christoph').toMatch(/stop/);  
});

错误

若你想测试某函数在调用时是否抛出了错误,你需要使用 toThrow

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // 你可以使用一个字符串或者正则表示错误信息中必须包含的文字
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // Or you can match an exact error message using a regexp like below
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

测试异步代码

在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况。

Promise

为你的测试返回一个Promise,Jest会等待Promise的状态,如果Promise的状态变为rejected, 测试将会失败

例如,有一个名为fetchData的Promise, 假设它会返回内容为'peanut butter'的字符串 我们可以使用下面的测试代码︰

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

Async/Await

写异步测试用例时,可以在传递给test的函数前面加上async。 例如,可以用来测试相同的fetchData 方案︰

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

准备与收尾

重复设置

如果你有一些要为多次测试重复设置的工作,你可以使用 beforeEach 和 afterEach,两者用法相同

beforeEach(fn, timeout)

文件内每个测试开始前执行的钩子函数。 如果传入的回调函数返回值是 promise 或者 generator,Jest 会等待 promise resolve 再继续执行测试。

可选地传入第二个参数 timeout(毫秒) 指定函数执行超时时间。

一次性设置

在某些情况下,你只需要在文件的开头做一次设置。 如果这个通用设置是异步的,就比较麻烦,因为没办法每个用例都设置一遍,这样性能还很差。Jest提供了beforeAllafterAll

beforeAll(fn, timeout)

文件内所有测试开始前执行的钩子函数。 如果传入的回调函数返回值是 promise 或者 generator,Jest 会等待 promise resolve 再继续执行。

可选地传入第二个参数 timeout(毫秒) 指定函数执行超时时间.

使用 beforeAll 会非常方便你设置一些在测试用例之间共享的全局状态。

作用域

顶层的 before*after* hook 函数会应用于文件中的每一条测试 在 describe 块中声明的 hook 函数,只会作用于 describe 中的测试

describe('matching cities to foods', () => {  
    // Applies only to tests in this describe block  
    beforeEach(() => {  
        return initializeFoodDatabase();  
    });  

    test('Vienna <3 veal', () => {  
        expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);  
    });  

    test('San Juan <3 plantains', () => {  
        expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);  
    });  
});

注意顶级的beforeEach 会比 describe 中的beforeEach 执行的更早。

模拟函数

Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。

有两种方法可以模拟函数:要么在测试代码中创建一个 mock 函数,要么编写一个手动 mock来覆盖模块依赖。

使用mock函数

在项目中,经常会碰见A模块掉B模块的方法。并且,在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,就需要mock函数了。

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

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

jest.fn()

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

// functions.test.js

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);
})

jest.fn()可以初始化时候不传入参数,然后通过调用生成的mock函数的mockImplementation或者mockImplementationOnce方法来改变mock函数内容,这两个方法的区别是

mockImplementationOnce只会改变要mock的函数一次:

test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    const a = run(func)
    const b = run(func)
    const c = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
})

image.png 我们可以看到,函数执行的结果第一次是this is mock fn 2,之后都是this is mock fn 1

同样的,我们可以使用mock函数的mockReturnValuemockReturnValueOnce(一次)方法来改变函数的返回值:

test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    func.mockReturnValue('this is mock fn 3')
    func.mockReturnValueOnce('this is mock fn 4')
        .mockReturnValueOnce('this is mock fn 5')
        .mockReturnValueOnce('this is mock fn 6')
    const a = run(func)
    const b = run(func)
    const c = run(func)
    const d = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
    console.log(d)
})
// functions.test.js
test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('default');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为default
  expect(result).toBe('default');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

.mock属性

所有的 mock 函数都有这个特殊的.mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息.mock属性还追踪每次调用时this的值,所以我们同样可以也检视(inspect) this:

const myMock1 = jest.fn();  
const a = new myMock1();  
console.log(myMock1.mock.instances);  
// > [ <a> ]  
  
const myMock2 = jest.fn();  
const b = {};  
const bound = myMock2.bind(b);  
bound();  
console.log(myMock2.mock.contexts);   
// > [ <b> ] 一个包含mock函数所有调用上下文的数组。

Mock的返回值

Mock 函数也可以用于在测试期间将测试值注入代码︰

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

模拟模块

假定有个从 API 获取用户的类。 该类用axios调用 API 然后返回 data,其中包含所有用户的属性:

import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

现在,为测试该方法而不实际调用 API (使测试缓慢与脆弱),我们可以用 jest.mock(...) 函数自动模拟 axios 模块

一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想说的是我们想让axios.get('/users.json') 有个伪造的响应结果。

import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});