单元测试

2020/11/18 tools

jest测试工具

1、测试

测试的工作就是在开发完成后项目上线前把守最后一关,当项目比较小,或者迭代不多的项目没必要用测试工具进行测试,在开发组件类库、框架之类的代码的时候防止别人的代价或第三方库插件影响自己的核心逻辑,用测试工具测一下。

测试会浪费大量的开发时间,测试的代码可能要比开发的代码还要多,所以小项目不咋迭代的没必要上。

缺点是不少,好处也挺重要。

  1. 快速定位bug
  2. 提高代码质量
  3. 方便迭代、重构
  4. 减少回归流程

2、测试分类

  1. 黑盒测试:功能测试,在不知道代码的情况下进行的测试。
  2. 白盒测试:了解代码的具体逻辑的情况下进行测试。
  3. 灰盒测试:灰盒测试多用于集成测试阶段,不仅关注输出、输入的正确性,同时也关注程序内部的情况。
  4. 单元测试:以最小的单元作为测试的目标进行测试。
  5. 集成测试:组件什么的放到一起进行测试。
  6. 其他:冒烟测试、UI测试、手工、自动化测试等等

3、测试思想/方式

  1. TDD:测试驱动开发,先写测试再写代码,有利于更加专注软件设计,清晰地了解软件的需求,很好的诠释了代码即文档。
  2. BDD:行为驱动开发,根据用户的行为进行测试,是一种敏捷软件开发的技术。设计测试用例的时候对系统进行定义,倡导使用通用的语言将系统的行为描述出来,将系统设计和测试用例结合起来,从而以此为驱动进行开发工作。

4、常见的测试框架

  1. Mocha:提供了一个测试环境,断言库(chai),mock(sinon)之类的需要较多配置来实现扩展
  2. karma:可以在真实浏览器中测试,一般用来测试UI组件,一般配合Mocha\jasmine进行使用。
  3. Ava:更轻量的单测框架。
  4. Jasmine:jest的前辈,异步测试支持不好。
  5. Jest:Facebook出的,基于jsdom的,用js来模拟浏览器环境,但是不能测样式相关,不需要额外集成或配置,默认支持断言,mock之类的。

5、Jest的安装及使用

官网建议使用yarn,使用npm也行,我这里就用yarn了。由于jest默认只支持node模块化的语法,所以还需要再安装一个Babel,再安装一个语法提示插件types/jest

// 初始化
yarn init -y
yarn add --dev jest


// 安装babel
yarn add @babel/preset-env

// 安装语法提示插件
yarn add @types/jest

配置.babelrc

{
  "presets": [
    ["@babel/preset-env",{
      "targets": {
        "node": "current"
      }
    }]
  ]
}

配置package.json

"scripts": {
  "test": "jest"
},

5.1 测试demo

创建src/sum.js

export function sum(a,b){
  return a + b;
}

创建sum.test.js

import { sum } from './src/sum';

describe('测试sum方法', () => {
  it('测试1+2=3', () => {
    expect(sum(1, 2)).toBe(3);
  })
})

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toEqual(3);
});

执行yarn test结果:

 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (1 ms)
  测试sum方法
    ✓ 测试1+2=3

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.276 s, estimated 1 s

每次修改需要编译很烦,可以在运行的时候使用yarn jest --watchAll,开启监控,每次修改保存文件后自动进行测试。

6、常用匹配器

先简单介绍一下上文提到的关键字

  1. describe描述, decribe会形成一个作用域
  2. it断言
  3. expect期望,expect(sum(1, 2)).toBe(3)的意思是 预计sum(1,2)的值严格等于3。
  4. test测试,类似于it

常用匹配器

  1. expect(sum(1, 2)).toBe(3):判断是否严格相等,普通值使用
  2. expect(sum(1, 2)).toEqual(3):判断是否相等, 引用值、对象使用
  3. expect(true).toBeTruthy():判断是否为true
  4. expect(false).toBeFalsy():判断是否为false
  5. expect(1+1).not.toBe(3):判断是否不等于3
  6. expect(1).toBeLessThan(3):判断是否小于
  7. expect(1).toBeLessThanOrEqual(3):小于等于
  8. expect(4).toBeGreaterThan(3):判断是否大于
  9. expect(4).toBeGreaterThanOrEqual(3):判断大于等于
  10. toBeCloseTo:浮点数相等
  11. toMatch:正则匹配
  12. toContain:判断是否包含特定项

其他更多:https://www.jianshu.com/p/c1b5676c1edd

7、操作

当开启了watchAll之后会有其他提示

  • › Press f to run only failed tests. 按F只运行错误的
  • › Press o to only run tests related to changed files. 按O只运行改变了的,需要配合git
  • › Press p to filter by a filename regex pattern. 按p根据文件名进行匹配
  • › Press t to filter by a test name regex pattern. 按t根据正则进行匹配
  • › Press q to quit watch mode. 按q退出当前
  • › Press Enter to trigger a test run. 按回车再跑一次
  • 可以在测试文件里面给某一个测试项添加only,该文件运行的时候只执行这一项测试
describe('测试sum方法', () => {
  it.only('测试1+2=3', () => {
    expect(sum(1, 2)).toBe(3);
  })
})

8、测试异步

使用一个名为done的参数后,jest将等待异步执行完成后进行测试。就是说,当done()执行的时候才是进行测试的时候,如果 done() 从未被调用过,那么测试就会失败。

it('测试promise', (done)=>{
  promiseFn().then(res=>{
    // res == {a:1};
    expect(res).toEqual({a:1});
    done();
  })
})

或者使用async / await进行测试

it('测试promise', async ()=>{
  let res = await promiseFn();
  expect(res).toEqual({a:1});
})

9、mock函数

jest.fn():模拟函数,可以记录被执行的过程, 模拟的函数给用户调用,调用过程中这个函数的所有信息会被记录到当前函数的mock属性上。

it('测试',()=>{
  let fn = jest.fn();
  userFn(1, fn);
  console.log(fn);
  expect(fn.mock.calls[0][0]).toBe(1);  // 通过
})

// 被测试的方法
function userFn(data, cb){
  cb(data);
}

打印fn的值

{
  calls: [ [ 1 ] ],
  instances: [ undefined ],
  invocationCallOrder: [ 1 ],
  results: [ { type: 'return', value: undefined } ]
}

其他更多的属性or方法,看文档https://www.jestjs.cn/docs/mock-functions

10、测试覆盖率

测试覆盖率(coverage):查看是否覆盖了整个程序

使用需要先初始化一个jest.config.js文件

npx jest --init

会提示是否用一些配置,比如是否用ts,是node环境还是jsdom环境等等,看需求自定义即可。

配置package.json/scripts

"scripts": {
  "test": "jest --coverage"
},

然后执行npm run test,会产生一个报表,打印类似下面内容

> jest@1.0.0 test 文件地址
> jest --coverage

 PASS  ./sum.test.js
  ✓ 测试 (2 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |       0 |      100 |       0 |       0 |                   
 sum.js   |       0 |      100 |       0 |       0 | 2                 
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.358 s

通过这个报表就可以看到有什么内容没测到。不过也不一定是越高越好,单元测试的覆盖率一般会比较高,集成测试不会很高。

11、mock数据

有时候需要mock一些接口,返回固定的数据看功能是否好使。

11.1 mock文件

假设src/ajax.js有一个功能块,需要测接口。

// src/ajax.js
import axios from 'axios';

export const fetchData = ()=>{
  return axios.get('/data');
}

外面测试的文件ajax.test.js

// ./ajax.test.js
import {fetchData} from './src/ajax';

it('测试接口', async () => {
  let res = await fetchData();
  // 返回的数据为['a','b']
  expect(res).toEqual(['a','b']);
})

这个时候要去mock一个接口,我们需要创建文件夹src/__mocks__,然后在下面创建一个同名的js文件src/__mocks__/ajax.js; 文件夹名字是固定的,下面的文件也需要同名。

// src/__mocks__/ajax.js
export const fetchData = () => {
  return new Promise((resolve, reject) => {
    resolve(['a', 'b']);
  })
}

然后在ajax.test.js文件里jest.mock()一下。意思是用__mocks__/ajax.js替换原来的文件,进行测试。

// ./ajax.test.js
jest.mock('./src/ajax.js');

import {fetchData} from './src/ajax';

it('测试接口', async () => {
  let res = await fetchData();
  expect(res).toEqual(['a','b']);
})

然后运行yarn jest 就可以看到测试通过了。这种方式在整个文件替换或简单的时候可以操作,我们还可以用其他方式来实现。

11.2 mock方法

上面是mock文件,然后在文件里操作,mock一个数据返回,我们可以搞一个假的axios,mock一个方法,方法里返回。

./ajax.test.js文件里的jest.mock('./src/ajax.js');删掉,创建src/__mocks__/axios.js

// src/__mocks__/axios.js
export default {
  get(url) {
    if (url == '/data') {
      return new Promise((res) => res(['a', 'b']))
    }
  }
}

然后再执行,结果还是一样的。

Search

    Table of Contents