Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
Jest是一个关注于简单使用且令人愉悦的JavaScript测试框架,它能够和Babel、TypeScript、Node、React、Angular、Vue等项目配合使用;
npm i -D jest
{
"scripts": { "test": "jest" }
}
npm i -D @babel/preset-env @babel/core @babel/preset-typescript npm i -D babel-jest
配置babel.config.js:react和typescript,配置env当process.env.NODE_ENV是test时对.less、.sass、.styl进行忽略避免在jest测试时对样式文件作出响应。
// babel.config.js
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react'
],
env: {
test: {
plugins: ['babel-plugin-transform-require-ignore', {
extensions: ['.less', '.sass', '.styl']
}]
}
}
};
配置jest.config.js
module.exports = {
rootDir: './test/', // 测试目录
// 对jsx、tsx、js、ts文件采用babel-jest进行转换
transform: {
'^.+\\.[t|j]sx?$': 'babel-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]s?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverage: true, // 统计覆盖率
testEnvironment: 'jsdom', // 测试环境,默认为”jsdom“
collectCoverageFrom: ['**/*.ts'],
coverageDirectory: './coverage', // 测试覆盖率的文档
globals: {
window: {}, // 设置全局对象window
},
setupFiles: ['<rootDir>/setupFiles/shim.js'],
// 测试前执行的文件,主要可以补齐模拟一些在node环境下的方法但又window下需要使用
};
// shim.js
// 在测试环境下模拟requestAnimationFrame函数
global.window.requestAnimationFrame = function (cb) {
return setTimeout(() => {
cb();
}, 0);
};
使用Matchers编写测试用例,例如:
test('测试用例名称', () => {
expect(2 + 2).toBe(4);
expect(2 + 2).not.toBe(3);
expect({ one: 1, two: 2 }).toEqual({ one: 1, two: 2 });
expect(null).toBeNull();
expect(null).toBeUndefined();
expect(null).toBeTruthy();
expect(null).toBeFalsy();
})
toBe使用的是Object.is所以可以用来测试非引用变量,当要测试Object时应该使用toEqual,这个方法会递归的进行比较。
toBeNull、toBeUndefined、toBeTruthy、ToBeFalsy是测试一些边界情况使用的API
下面是关于数字类型的API,语义非常明显不再需要解释
test('测试类型的API', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
});
更多类型的matcher将写在附录中,尽情参考
默认的jest代码是一下执行到最后的,所以通常下面的代码是无法被正确测试的
// 不会生效
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
而下面的才能生效,通过done告诉jest是否测试完毕
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
同样的也是支持promise的测试
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
// resolve和reject两种情况进行测试
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
// 或者结合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.toThrow('error');
});
那么可以使用beforeEach
、afterEach
、beforeAll
、afterAll
、describe
通过Each在每个test前后进行执行,通过All在当前文件的所有test前后进行执行
,由describe包裹起来的部分,将形成一个scope使得All和Each只当前scope内会生效。示例如下:
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
jest.fn()
能够创建一个mock函数,它的.mock属性保存了函数被调用的信息,还追踪了每次调用的this值。
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
test('该模拟函数被调用了两次', () => {
// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
})
test('第一次调用函数时的第一个参数是0', () => {
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
})
test('第二次调用函数时的第一次参数是1', () => {
// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
})
// 表示被调用的次数
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);
// [n]表示第几次调用[m]表示第几个参数
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// 被实例化两次
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// 第一次实例化返回的对象有一个name属性并且值是test
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');
上述对于mock的简单说明其实看过一遍是很懵的无法直到它的实际用途,那么下面我将列举一个我在开发中使用jest.fn()模拟函数来对方法进行单元测试的例子:
首先我们有一个功能,目的是能使页面滑动到某一个位置,最顶部或者是中间或者是底部,这个函数通常都被用于展示在页面上的回到顶部按钮。
函数类似下面这样
export const scrollTo: (y?: number, option?: { immediately: boolean }) => void = (
y = 0,
option = { immediately: false }
) => {
if (option.immediately) {
window.scrollTo(0, y);
return;
}
const top = document.body.scrollTop || document.documentElement.scrollTop;
const clientHeight = document.body.clientHeight || document.documentElement.clientHeight;
const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight;
if (top === y) {
return;
}
if (y > scrollHeight - clientHeight && y <= scrollHeight) {
y = scrollHeight - clientHeight;
}
if (Math.abs(top - y) > 1) {
let movDistance = Math.ceil((y - top) / 8);
let nextDestination = top + movDistance;
if (Math.abs(top - y) < 8) {
nextDestination = y;
}
window.requestAnimationFrame(scrollTo.bind(this, y));
window.scrollTo(0, nextDestination);
}
};
我们可以明确的知道函数里读取了scrollTop、clientHeight、scrollHegith和使用了window.requestAnimationFrame、window.scrollTo方法。这三个属性和这两个方法在node环境中其实都是没有的。
针对window.requestAnimationFrame其实我们在上面的shim.js已经做了模拟,方法的具体多久执行我们不需要在意,只需要知道过一段时间他就应该执行其就可以了,所以使用了setTimeout来进行模拟,这个api在node端也是存在的。那么对于其他的属性和方法,我们可以有下面的测试代码。
const scrollHeight = 8000;
const clientHeight = 1000;
const fakeWindow = {
scrollTop: 0,
};
beforeAll(() => {
// 定义网页整体高度
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => scrollHeight);
// 定义窗口高度
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => clientHeight);
// 劫持scrollTop的获取,存放在fakeWindow里
jest.spyOn(document.documentElement, 'scrollTop', 'get').mockImplementation(() => fakeWindow.scrollTop);
// 或者像下面这样去操作
});
describe('测试立即达到对应位置', () => {
beforeEach(() => {
fakeWindow.scrollTop = 1000;
});
test('立即回到顶部', () => {
const mockScrollTo = jest.fn().mockImplementation((x = 0, y = 0) => {
fakeWindow.scrollTop = y;
});
global.window.scrollTo = mockScrollTo;
scrollTo(0, { immediately: true });
const length = mockScrollTo.mock.calls.length;
expect(mockScrollTo.mock.calls[length - 1][0]).toEqual(0);
});
});
这样我们就通过jest.spyOn()、jest.fn()方法完成了我们对scrollTo方法的滑动到顶部的单元测试,再添加更多的滑动到底部、中间等测试,我们就算完成了这个方法的基本情况测试,在生产环境使用也就更加自信拉~
待补充
待补充