前端代码接入单元测试

举报
云端小宅女 发表于 2021/07/29 09:42:45 2021/07/29
【摘要】 本篇从基础讲解然后到前端的react单元测试及node的单元测试。

一、单元测试发展

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。