前端自动化测试工具Jest基础

114 阅读10分钟

1.1 前端自动化测试背景

开发复杂的业务场景时或者对老代码进行修改时,容易产生Bug,为了在上线之前发现Bug,可以采取的措施:

  • 通过测试同学的验证发现
  • 通过多人code view发现
  • 灰度发布
  • 借助工具TypeScript、Flow、ESlint、StyleLint避免
  • 使用前端自动化测试工具,进一步发现Bug,单元测试、集成测试、端到端的测试

没有前端自动化测试时,只有验证功能出现问题或者线上产生了Bug才能发现代码中的问题。

使用前端自动化测试,可以写测试代码,通过测试代码运行业务功能中编写的代码,检查代码输出的结果是否和预期的是一致的

1.2 前端自动化测试框架Jest

一款优雅、简洁的JavaScript的测试框架,支持Babel、TybeScript、Node、React、Angular、Vue等诸多框架。

优点:

  • 速度快、API简单、易配置、隔离性好 (windows上运行感觉不快啊?)
  • 监控模式、IDEA整合、Snapshot、多项目并行、覆盖率、Mock丰富

安装:

npm init
npm install jest@24.8.0 -D # -D 只在开发环境下才会运行单元测试
npm install jest@24.8.0 --save-dev # 同样只在开发环境运行
"test": "jest" # package.json中scripts中增加命令

运行一个案例:

待测试逻辑代码:

function add(a, b) {
  return a + b
}
function sub(a, b) {
  return a - b
}
function multi(a, b) {
  return a * b
}
module.exports = {
    add,
    sub,
    multi
  }

测试代码:

const { add, sub, multi } = require('./math')
test('测试加法1 + 2', () => {
  expect(add(1, 2)).toBe(3)
})
test('测试减法7 - 3', () => {
  expect(sub(7, 3)).toBe(4)
})
test('测试乘法2 * 5', () => {
  expect(multi(2, 5)).toBe(10)
})

运行测试代码:

> lesson2@1.0.0 test E:\project\frontendautomatedtest\lesson2
> jest
PASS ./math.test.js
√ 测试加法1 + 2 (4ms)
√ 测试减法7 - 3
√ 测试乘法2 * 5 (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 7.226s
Ran all test suites.

1.3 Jest的简单配置

生成基础配置文件
npx jest --init # 暴露jest配置,jest.config.js

在jest.config.js中可以指定生成测试覆盖率报告的保存路径

coverageDirectory: "coverage" # 测试覆盖率报告路径在当前文件夹下的coverage下
npx jest --coverage # 运行命令,执行测试,并生成测试覆盖率报告
使用Babel
# 1. 安装所需要的依赖
npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D # 引入babel相关的库,将math.js和math.test.js 修改为ESModule导出和导入的形式,因为node环境下仅支持commonjs的语法,参考代码lesson3
# 2. 新建.babelrc babel配置文件
{
  "presets": [ # 插件预设 预设 一组插件集合
    ["@babel/preset-env", { # 内部数组的第二项是当前预设的配置
      "targets": { # 根据当前node环境使用@babel/preset-env转换当前的代码,例如将import语法转成
        "node": "current" # commonsjs的语法
      }
    }]
  ]
}
# 3. 运行测试,看ESModule导出和导入的语法是否会报错
npm run test

讲师分析原理,当运行npm run jest时,jest中的babel-jest模块,会检测当前目录下是否包含bable和bable-core依赖,有的话,就取.babelrc中的配置,在运行测试之前,使用babel 先把待测试代码做一次转化,然后再运行转换后的测试用例代码。

1.4 Jest中常用的匹配器

Jest中提供了大量的匹配器,用于判断测试案例运行的实际结果和预期结果是否匹配,Jest会跟踪所有失败的匹配器,然后打印出明确的错误消息。

常用的匹配器如下:

toBe 精确匹配,类似于Object.is,如果是对象或者数组的话,需要都是同一个对象或者数组的引用

toEqual 可用于检查对象或者数组的值(内容)是否相同,会递归的检查每个字段

toBeNull 只匹配null

toBeUndefined 只匹配undefined

toBeDefined 与toBeUndefined相反

toBeTruthy 真,匹配任何if语句为真

toBeFalsy 假,与toBeTruthy相反

not 非,反向匹配器

toBeGreaterThan 大于

toBeLessThan 小于

toBeGreaterThanOrEqual 大于等于

toBeLessThanOrEqual 小于等于

toBeCloseTo 比较浮点数相等,避免浮点数运算的舍入误差,例如0.1 + 0.2 === 0.3

toMatch 检查字符串匹配 支持正则

toContain 检查数组或集合是否包含某一项 ,支持正则

toThrow 检查函数在调用时,是否抛出了某个个异常,支持正则

所有匹配器,参考官方文档Expect断言

1.5 Jest命令行工具使用

Jest命令行工具可以指定选项,从而按照指定的方式运行测试。

常用命令说明:
jest # 运行所有测试文件
jest matcher.test.js # 只运行matcher.test.js这个测试文件
jest -o # 只运行git追踪到改到的文件,项目需要已经创建了git仓库
jest -t toMatch # 运行匹配test或describe名称中包含toMatch字符串的测试案例
jest --watch # 监视当前文件加下有改动的测试文件 类似jest -o
jest --watchAll # 监视所有测试

选项比较多,可以使用jest --help查看所有命令选项,也可以参考官方文档Jest CLI选项

使用jest --watch启动监控模式后,会显示如下选项,可以选择不同的模式运行测试文件

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
模式说明如下:

a模式:每次运行所有的测试文件

f模式:进运行失败的测试文件

p模式 :配合测试文件的文件名,输入正则表达式,过滤文件名

t模式 :匹配test的名称,输入正则表达式,过滤test名称

q:退出watch模式

1.6 测试异步代码

默认情况下,当测试代码运行到上下文的底部时,Jest认为测试代码执行完成了,但是此时异步代码还未执行完成,Jest官网建议使用以下三种方法解决这个问题。

方法1 使用done回调

Jest会等done回调函数执行结束后,再结束测试。

test('fetchData返回结果为 { success:true }', done => {
  // 添加一个回调参数done,只有执行了done才认为这个测试用例结束了
  fetchData(data => {
    expect(data).toEqual({
      success: true
    })
    done()
  })
})
方法2 返回Promise

Jest会等Promised的resolve状态,如果Promise的状态变成了rejected,测试将会失败。

test('fetchDataPromise { success: true }', () => {
  // return 一个promise
  return fetchDataPromise().then(res => {
    expect(res.data).toEqual({ success: true })
  })
})
方法3 使用Async/Await
test('fetchDataPromise { success: true }', () => {
  // return 一个promise
  return fetchDataPromise().then(res => {
    expect(res.data).toEqual({ success: true })
  })
})

如果期望Promise被reject,则需要使用catch方法,请确保添加了expect.assertions检查断言被调用的次数,否则变成fulfilled状态的Promise不会让测试用例失败。

test('fetchDataPromise404 返回404', async () => {
  expect.assertions(1)
 try {
  await fetchDataPromise404()
 } catch (e) {
  expect(e.toString()).toEqual('Error: Request failed with status code 404')
 }
})

也可以将 async and await.resolves or .rejects一起使用,来处理异步期望的结果

test('fetchDataPromise404 返回404', async () => {
  return expect(fetchDataPromise404()).rejects.toThrow()
})

1.7 Jest中的钩子函数

如果需要在运行测试案例之前,准备一些测试数据或做一些准备工作,Jest提供了一些辅助函数,如beforeAll、beforeEach、afterEach、afterAll,类似vue-router中的路由钩子函数。

beforeAll 在运行所有test案例之前运行一次,可以做所有测试开始前的准备工作

afterAll 在运行完成所有test案例之前运行一次,可以做所有测试完成后的清理工作

beforeEach 在运行每一个test案例之前都会被调用

afterEach 运行完每一个test案例之后都会被调用

作用域说明

在文件顶层定义的before和after的hook函数会作用在每个test案例上,在describe块中声明的hook函数,只会作用于describe中的test案例上。

并且,顶级的beforeEach比describe中的beforeEach执行的早

执行顺序

在运行test测试案例之前,会先执行describe中的代码,describe中的代码执行完成后,默认情况下Jest会按照顺序依次运行test案例。

如下案例:

describe('describe outer', () => {
  console.log('describe outer-a');
  describe('describe inner 1', () => {
    console.log('describe inner 1');
    test('test 1', () => console.log('test 1'));
  });
  console.log('describe outer-b');
  test('test 2', () => console.log('test 2'));
  describe('describe inner 2', () => {
    console.log('describe inner 2');
    test('test 3', () => console.log('test 3'));
  });
  console.log('describe outer-c');
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3

如果运行了较为复杂的test,出现失败的情况后,官网建议使用test.only仅运行此条测试观察是否会报错,如果没有报错,然后运行整个测试文件会报错,可以考虑是否是外部条件的设置导致的。

1.8 Jest中的模拟函数Mock

模拟函数调用

有时我们需要模拟函数的实现,比如函数的返回值,函数被调用了多少次,函数调用时传入的参数等情况,Jest提供了Mock模拟函数jest.fn()来帮助我们测试函数的调用情况。

例如我们模拟一个返回2次方的函数,但是要求,入参是2返回1,入参是3返回2,入参是4返回函数本身执行的结果,代码如下:

test('模拟2次方', () => {
  const fn = jest.fn(num => {
    return Math.pow(num, 2)
  })
  fn.mockReturnValueOnce(1) // 模拟一次函数返回结果是1
  expect(fn(2)).toBe(1)
  fn.mockReturnValueOnce(2) // 模拟一次函数返回结果是2
  expect(fn(3)).toBe(2)
  expect(fn(4)).toBe(16)
  console.log(fn.mock)
})

在上面的例子中,我们打印了模拟函数fn的mock属性,mock属性是所有模拟函数都有的一个属性,它保存了模拟函数每次被调用时的入参、调用方、调用顺序和返回结果。上面的打印信息如下:

{
  calls: [ [ 2 ], [ 3 ], [ 4 ] ], # 每次调用时的入参
  instances: [ undefined, undefined, undefined ], # 调用方法,this,此处是node环境
  invocationCallOrder: [ 1, 2, 3 ], # 调用顺序
  results: [ # 每次调用时的返回结果
    { type: 'return', value: 1 },
    { type: 'return', value: 2 },
    { type: 'return', value: 16 }
  ]
}

因此,根据这些信息就可以测试函数的调用过程,例如

...
expect(fn.mock.calls).toHaveLength(3) // 判定函数被调用了3次
expect(fn.mock.calls[2]).toEqual([4]) // 判定第3次调用时,入参为4
expect(fn.mock.results[2].value).toBe(16) // 判定第3次调用,返回值为16

// 检查this指向
test("检查this", () => {
  const Dog = jest.fn();
  const a = new Dog();
  const b = new Dog();
  expect(Dog.mock.instances[0]).toBe(a);
  expect(Dog.mock.instances[1]).toBe(b);

  const Cat = jest.fn();
  const c = {};
  const bound = Cat.bind(c);
  bound();
});
模拟模块

几乎所有的前端项目都会有一个请求后端数据的模块requestUtil,假设该模块有个获取用户信息的getUserInfo方法,该方法调用了axios的get方法请求后端接口,返回用户的信息数据data。为测试该方法而不实际调用 API (使测试缓慢与脆弱),可以用 jest.mock(...) 函数自动模拟 axios 模块。

import { getUserInfo } from './requestUtil'
import axios from "axios"
jest.mock('axios')
test('测试getUserInfo方法', async () => {
  axios.get.mockResolvedValueOnce({data: { nickName: 'hewang', id: 1}})
  axios.get.mockResolvedValueOnce({data: { nickName: '曹操', id: 2}})
  await getUserInfo().then(data => {
    expect(data).toEqual({ nickName: 'hewang', id: 1})
  })
  await getUserInfo().then(data => {
    expect(data).toEqual({ nickName: '曹操', id: 2})
  })
})

Jest还提供了很多匹配器,方便判定模拟函数的调用情况

// mock方法至少被调用一次
expect(mockFunc).toHaveBeenCalled();
// mock方法至少被调用一次并且入参为arg1和arg2
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// 最后一次调用入参是arg1和arg2
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

代码参考gitee仓库

参考文档Jest官方文档