前端代码接入单元测试
一、单元测试发展
1、为什么要有单元测试
软件测试是一种实际输出与预期输出之间的审核或者比较过程
测试可以尽早发现BUG
测试可以提高代码质量
测试可以让我们自信地重构
2、手动的测试代码(或者叫肉眼测试)
function add(a, b) {
return a + b;
}
console.assert(add(4, 2) == 7, '测试add函数');
3、测试框架与手工断言的对比
手工断言 | 测试框架 |
---|---|
污染源代码 | 可能分离测试代码和源代码 |
散落在各个文件中 | 测试代码可以集中存放 |
没有办法持久化保存 | 放置到单独的文件中 |
手动执行和对比麻烦不自动 | 可以自动运行、显示测试结果 |
二、Jest框架的使用
1、Jest由Facebook出品,非常适合React测试、零配置、内置代码覆盖率、强大的Mocks
2、官网地址
3、在项目中使用
npm install jest -D
4、在文件的旁边创建一个xx.test.js的文件
// match.js文件
function add(a, b) {
return a + b;
}
function minus(a, b) {
return a - b;
}
module.exports = {
add,
minus,
};
// math.test.js文件
let { add, minus } = require('./match');
describe('测试add', function () {
test('测试1+1', function () {
expect(add(1, 1)).toBe(2);
});
test('测试2+2', function () {
expect(add(2, 2)).toBe(4);
});
});
describe('测试minus', function () {
test('测试1-1', function () {
expect(minus(1, 1)).toBe(0);
});
test('测试2-2', function () {
expect(minus(2, 2)).toBe(0);
});
});
5、在package.json中配置jest测试命令
"scripts": {
"test1":"jest",
"test": "jest --watchAll" // 可以监控代码改变自动运行测试
},
6、使用jest生成测试报告
代码覆盖率是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率
"scripts": {
"test1": "jest",
"test": "jest --watchAll",
+ "coverage": "jest --coverage"
},
运行命令会在项目下生成一个Icov-report/index.html的文件,运行这个文件
控制面板上会有对应的信息输出
类型 | 说明 |
---|---|
line coverage | 行覆盖率 |
function coverage | 函数覆盖率 |
branch coverage | 分支覆盖率 |
statement coverage | 语句覆盖率 |
7、常见的匹配器
https://jestjs.io/zh-Hans/docs/using-matchers
8、全部的匹配器
https://jestjs.io/zh-Hans/docs/expect
三、使用babel和typescript来做单元测试
1、安装依赖包,参考文档https://jestjs.io/zh-Hans/docs/getting-started#%E4%BD%BF%E7%94%A8-babel
yarn add jest ts-node babel-jest @types/jest @babel/core @babel/preset-env @babel/preset-typescript typescript -D
2、生成tscconfig.json文件
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "src"
},
"exclude": ["node_modules"],
"include": ["./src/**/*.ts", "__tests__/**/*.ts"]
}
3、根目录下创建一个babel.config.js的文件
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
'@babel/preset-typescript',
],
};
4、初始化jest文件
npx jest --init
// jest.config.ts文件内容
export default {
// 用来检测测试文件的glob模式,
// 单元测试文件可以是在__tests__文件夹下以js、ts、jsx、tsx结尾的文件,也可以是spec.ts和test.ts结尾的文件
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[tj]s?(x)',
],
// 在每一次测试时自动清除mock调用和实例
clearMocks: true,
collectCoverage: true,
// 输出代码覆盖率的目录
coverageDirectory: 'coverage',
// 使用的模块的文件扩展名数组
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
// 用来跑测试的测试环境,可以选择jsdom或node
testEnvironment: 'jsdom',
};
5、在src目录下创建match.ts和match.spec.ts文件
// match.ts文件代码
export const add = (a: number, b: number): number => {
return a + b;
};
export const minus = (a: number, b: number): number => {
return a - b;
};
// match.spec.ts文件代码
import { add, minus } from './match';
describe('测试add', function () {
test('测试1+1', function () {
expect(add(1, 1)).toBe(2);
});
test('测试2+2', function () {
expect(add(2, 2)).toBe(4);
});
});
describe('测试minus', function () {
test('测试1-1', function () {
expect(minus(1, 1)).toBe(0);
});
test('测试2-2', function () {
expect(minus(2, 2)).toBe(0);
});
});
6、也可以将测试代码放到__tests__目录下,文件名与src里面的文件名保持一致
四、关于jsdom的使用
在jest初始化的时候会默认生成jsdom的,如果没有的话,可以在jest.config.ts中配置上testEnvironment: “jsdom”,jsdom官方地址
1、创建待测试的文件
/** 定义删除node节点 */
export const remove = (node: HTMLElement) => {
node.parentNode?.removeChild(node);
};
/**定义绑定事件 */
export const addEventListener = (
node: HTMLElement,
type: any,
listener: (this: HTMLElement, ev: MouseEvent) => any
) => {
node.addEventListener(type, listener);
};
2、单元测试文件
import { remove, addEventListener } from './../src/dom';
describe('dom节点测试', function () {
test('测试remove方法', () => {
document.body.innerHTML = `
<div id="parent">
<div id="children">子节点</div>
</div>
`;
const parentDom = document.getElementById('parent');
// 期望这个节点是div
expect(parentDom?.nodeName.toLocaleLowerCase()).toBe('div');
const childDom = document.getElementById('children');
expect(childDom?.nodeName.toLocaleLowerCase()).toBe('div');
remove(childDom!);
// 删除后期望是空节点
expect(document.getElementById('children')).toBeNull();
});
test('测试addEventListener方法', () => {
document.body.innerHTML = `
<div id="parent">
<button id="children">子节点</button>
</div>
`;
const childDom = document.getElementById('children');
expect(childDom).not.toBeNull();
addEventListener(childDom!, 'click', () => {
childDom!.innerHTML = '点击后的内容';
});
// 模拟点击
childDom?.click();
expect(childDom?.innerHTML).toBe('点击后的内容');
});
});
五、异步函数的测试
1、官方地址
https://jestjs.io/zh-Hans/docs/asynchronous
2、定义待测试的异步函数
export const callBack = (fn: Function) => {
setTimeout(() => {
fn({ code: 0 });
}, 3000);
};
3、测试代码
import { callBack } from './../src/callback';
describe('测试异步方法', () => {
test('测试异步', (done: Function) => {
// 调用callBack方法
callBack((response: Record<string, any>) => {
expect(response).toEqual({ code: 0 });
done();
});
});
});
六、测试promise
1、待测试的方法
export const callPromise = (): Promise<Record<string, any>> => {
return new Promise((resolve: Function) => {
setTimeout(() => resolve({ code: 0 }), 3000);
});
};
2、测试方法一
describe('测试promise', () => {
test('方法一', (done: Function) => {
callPromise().then((response: Record<string, any>) => {
expect(response).toEqual({ code: 0 });
done();
});
});
});
3、方式二
describe('测试promise', () => {
test('方法二', async () => {
const result: Record<string, number> = await callPromise();
expect(result).toEqual({ code: 0 });
});
});
七、Mock的使用
1、关于介绍可以参考官方地址
https://jestjs.io/zh-Hans/docs/mock-functions
2、官方的测试案例
export const forEachFn = (items: number[], callback: Function) => {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
};
import { forEachFn } from './../src/mock1';
describe('mock的测试', () => {
test('测试forEach函数', () => {
// mock一个函数
const mockFn = jest.fn((x: number) => 42 + x);
// 调用定义的方法
forEachFn([0, 1, 2], mockFn);
// 测试调用了几次
expect(mockFn.mock.calls.length).toBe(3);
// 调用函数传递的参数
expect(mockFn.mock.calls[0][0]).toBe(0);
expect(mockFn.mock.calls[1][0]).toBe(1);
expect(mockFn.mock.calls[2][0]).toBe(2);
// 调用函数的返回值
expect(mockFn.mock.results[0].value).toBe(42);
expect(mockFn.mock.results[1].value).toBe(43);
expect(mockFn.mock.results[2].value).toBe(44);
});
});
3、测试函数的返回值
/**测试函数 */
export const exec = (callback: (name: string) => string) => {
return callback('张三');
};
describe('测试函数', () => {
test('测试exec函数', () => {
// 模拟一个函数并且执行这个函数
const fn = jest.fn();
exec(fn);
// 测试是否调用
expect(fn).toBeCalled();
expect(fn).toHaveBeenCalled();
// 测试调用次数
expect(fn).toBeCalledTimes(1);
expect(fn).toBeCalledWith('张三');
// 测试返回值
fn.mockReturnValueOnce('张三');
expect(fn()).toBe('张三');
});
});
4、测试模拟接口请求,官方地址
import axios from 'axios';
/** 请求后端接口的方法 */
export const getUserList = async (): Promise<any> => {
return await axios.get('/user');
};
import axios from 'axios';
import { getUserList } from './../src/mock2';
jest.mock('axios');
describe('测试模拟接口', () => {
test('调用后端用户接口', async () => {
// 模拟数据
(axios.get as any).mockResolvedValue({
code: 0,
message: '成功请求',
result: {},
});
const result = await getUserList();
expect(result.code).toBe(0);
});
});
八、钩子函数的使用
1、钩子函数对不同测试执行阶段提供了对应的回调接口
2、beforeAll 在所有测试用例执行之前执行
3、beforeEach 每个测试用例执行前执行
4、afterEach 每个测试用例执行结束时
5、afterAll 等所有测试用例都执行之后执行
6、only的意思是只调用特定的测试用例
let count: number = 0;
describe('测试钩子函数', () => {
beforeAll(() => {
count++;
console.log('beforeAll钩子函数', count); // 1
});
afterAll(() => {
count++;
console.log('afterAll在全部的钩子之后执行', count); // 8
});
beforeEach(() => {
count++;
console.log('在每一个测试单元的时候执行', count); //2,4
});
afterEach(() => {
count++;
console.log('在每一个测试单元之后执行', count); // 5,7
});
describe('测试单元', () => {
test('测试单元1', () => {
count++;
console.log('测试单元1', count); // 3
});
test('测试单元2', () => {
count++;
console.log('测试单元2', count); //6
});
});
});
————————————————
版权声明:本文为CSDN博主「水痕01」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/kuangshp128/article/details/119183129
- 点赞
- 收藏
- 关注作者
评论(0)