Skip to main content

Jest

测试示例

// 测试流程
import fn from 'XX'; // 1. 导入要测试的函数

test('测试 fn 函数', () => {
expect( fn('参数1') ).toBe(100); // 2. 给函数传入对应的参数, 检测对应的结果
expect( fn('参数1', '参数2') ).not.toEqual(100); // 2. 给函数传入对应的参数, 检测对应的结果
});

test('2 + 2 等于 4', () => {
expect(2 + 2).toBe(4);
});

test('对象赋值', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2}); // toEqual 是 递归的检查对象或数组的每个字段
});

Expect API大全

toBe VS toEqual

toEqual 是递归的判断对象的每个值是否相等。 toBe 只判断对象的地址是否相同。

const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
const can3 = can1

describe('toBe VS toEqual', () => {
test('相等', () => {
expect(can1).toEqual(can2);
});
test('不相等', () => {
expect(can1).not.toBe(can2);
});
test('相等', () => {
expect(can1).toBe(can3);
});
});

判断真值

有时候我们想要区分 undefined null false, 但是有时候我们不希望区分

  • toBeNull 仅匹配 null
  • toBeUndefined 仅匹配 undefined
  • toBeDefinedtoBeUndefined结果相反
  • toBeTruthy 匹配 if 语句中视为真值的任何内容 (不区分)
  • toBeFalsy 匹配 if 语句中视为假值的任何内容 (不区分)
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});

test('zero', () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});

Number

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

// toBe | toEqual 用来判断相等
expect(value).toBe(4);
expect(value).toEqual(4);
});

// 浮点数判断 用 toBeCloseTo 代替 toEqual
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); // 错误, 因为精度丢失的原因
expect(value).toBeCloseTo(0.3); // 正确
});

String

test('使用 toMatch 来进行正则校验', () => {
expect('team').not.toMatch(/I/);
});

test('使用 toMatch 来进行正则校验', () => {
expect('Christoph').toMatch(/stop/);
});

Array 和 iterable

const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];

test('toContain 判断 arr 中是否存在 milk', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});

异常

如果您想测试特定函数在调用时是否抛出错误,使用 toThrow

// 抛出异常的函数需要在包装函数中调用,否则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/);
});

异步

Promise

从测试中返回一个Promise,Jest将等待该Promise解决。例如,我们假设fetchData返回一个promise,它被认为解析为字符串'peanut butter'。我们可以用

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

Async / Await

另外,您也可以在测试中使用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');
}
});

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

Callbacks

如果你不使用Promise,你可以使用回调。例如,fetchData不是返回一个promise,而是期望一个回调,即获取一些数据并在完成时调用callback(null, data)。您希望测试返回的数据是否是字符串'peanut butter'。默认情况下,Jest测试在执行结束时完成。这意味着这个测试无法按预期进行

// 不能这样使用。 问题是,在调用回调之前,测试将在fetchData完成后立即完成。
test('the data is peanut butter', () => {
function callback(error, data) {
if (error) {
throw error;
}
expect(data).toBe('peanut butter');
}

fetchData(callback);
});

// 有另一种测试形式可以解决这个问题。与其将测试放在带有空参数的函数中,不如使用一个名为done的参数。Jest将在完成测试之前等待,直到done回调被调用。
test('the data is peanut butter', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}

fetchData(callback);
});

如果从未调用 done(),则测试将失败(带有超时错误),这正是您希望发生的情况。 如果expect语句失败,它会抛出一个错误,并且不会调用done()。如果我们想在测试日志中看到它失败的原因,我们必须将expect 放在 try ... cache 块中,并将catch块中的错误传递给done

Setup & Teardown

通常在编写测试时,在测试运行之前需要进行一些设置工作,在测试运行之后需要进行一些完成工作。Jest提供了帮助函数来处理这个问题。

完整介绍

重复设置

如果您需要对许多测试重复执行一些工作,您可以使用 beforeEach 和 afterEach 钩子。例如,假设多个测试与数据库交互。在每个测试之前都必须调用一个方法initializeCityDatabase(),在每个测试之后都必须调用一个方法clearCityDatabase()。你可以用

beforeEach(() => {
initializeCityDatabase();
});

afterEach(() => {
clearCityDatabase();
});

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

一次性设置

beforeAll(() => {
return initializeCityDatabase();
});

afterAll(() => {
return clearCityDatabase();
});

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

作用域

beforeEach(() => {
return initializeCityDatabase();
});

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

describe('matching cities to foods', () => {
// 仅应用于此描述块中的测试
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);
});
});

模拟函数

模拟函数允许通过以下方式测试代码之间的联系 : 删除函数的实际实现、捕获对函数的调用(以及在这些调用中传递的参数)、在使用new实例化时捕获构造函数的实例,以及允许对返回值进行测试时配置。

有两种方法来模拟函数:创建一个在测试代码中使用的模拟函数,或者编写一个手动模拟来覆盖模块依赖项。

//  假设我们正在测试函数 forEach 的实现,该函数为所提供的数组中的每个项调用回调。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}

// 为了测试这个函数,我们可以使用一个模拟函数,并检查模拟的状态,以确保按预期调用回调函数。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// 模拟函数调用了2次
expect(mockCallback.mock.calls.length).toBe(2);

// 函数第一次调用的第一个参数是0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// 函数第二次调用的第一个参数是1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// 函数的第一次调用的返回值是42
expect(mockCallback.mock.results[0].value).toBe(42);

.mock 属性

所有模拟函数都有这个特殊的 .mock 属性,在该属性中保存有关函数如何调用以及函数返回内容的数据。mock属性还会对每次调用跟踪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> ]

这些模拟成员在测试中很有用,可以断言这些函数如何被调用、实例化或返回什么

// fn 只调用了一次
expect(someMockFunction.mock.calls.length).toBe(1);

// 函数第一次调用的第一个参数是'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// 函数第一次调用的第二个参数是'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// 函数第一次调用的返回值是“return value”
expect(someMockFunction.mock.results[0].value).toBe('return value');

// 函数被调用时带有特定的this上下文: element 对象。
expect(someMockFunction.mock.contexts[0]).toBe(element);

// 这个函数实例化了两次
expect(someMockFunction.mock.instances.length).toBe(2);

// 函数第一次实例化返回的对象拥有一个名为name的属性,其值被设置为test
expect(someMockFunction.mock.instances[0].name).toEqual('test');

// 函数最后一次调用的第一个参数是'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

模拟返回值

模拟函数还可以用于在测试期间向代码中注入测试值

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

// 第一次返回 true, 第二次返回 false
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

模拟包

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

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

export default Users;

// user.test.js
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);

// 或者您可以根据您的用例使用以下内容, axios.get.mockImplementation(() => Promise.resolve(resp))

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

模拟部分函数

// foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';

//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');

return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

Mock Implementations

在某些情况下,超出指定返回值的能力并完全替换模拟函数的实现是有用的。这可以用 jest.fn 来完成 或 mockImplementationOnce方法。

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val)); // > true

当您需要定义从另一个模块创建的模拟函数的默认实现时,mockImplementation方法非常有用

// foo.js
module.exports = function () {
// 执行一些代码;
};

// test.js
jest.mock('../foo');
const foo = require('../foo');

// foo 是模拟的fn
foo.mockImplementation(() => 42);
foo();// > 42

当您需要重新创建模拟函数的复杂行为,以便多个函数调用产生不同的结果时,请使用mockImplementationOnce方法

const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val)); // > true
myMockFn((err, val) => console.log(val)); // > false

///////////////
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

对于通常是链式方法(因此总是需要返回this)的情况,我们有一个语法糖来简化它,以 .mockReturnThis() 函数的形式,该函数也位于所有mock上

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// 和下面一样
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};