Jest单元测试

174 阅读9分钟

一、基本概念了解

1、瀑布式开发和敏捷开发的区别

区别瀑布式开发敏捷开发
概念严格按照需求分析-设计-编码-测试-维护的步骤顺序进行以用户的需求进化为核心,将一个大的项目拆分成多个相互联系但也可以独立运行的子项目,分别完成,具有更高的灵活性
优点①阶段清晰,安装顺序依次执行,环环相扣②黑盒模式,各阶段角色只需关注自身的分工①更快交付阶段性成果②开发人员对需求理解更高,适应性更强,变更风险低
缺点①需求隔离,开发人员对需求的理解程度高低不齐②变更代价大③束缚创造性①由于需求可能会有波动,很难把握整体产品的一致性,资源需要不断地重新分配②很难定义一些标准的流程规范
适用场景文档驱动,适用于toB的项目,如CRM、OA测试驱动,适用于需求不明确、创新性强的项目,或者需要抢占市场的项目

2、单元测试和e2e测试的区别:

区别单元测试e2e测试(端到端测试)
含义将代码分解为易于测试的小单元,可以是一个函数、组件、类功能测试,测试整个功能链路能不能走得通
优点①测试运行的很快②测试精确,定位到具体的函数①可以一次隐式测试很多东西②e2e测试可以确保拥有一个工作系统
缺点①耗时②尽管单元测试通过,整个应用程序可能还是无法正常工作①运行缓慢②无法查明失败的具体原因③测试很脆弱:如果只是一个组件调整逻辑,就需要重新设计e2e test
关注者开发人员一般更关注单元测试测试人员一般更关注端到端测试

3、TDD/BDD/DDD

区别测试驱动开发(TDD)行为驱动开发(BDD)领域驱动开发(DDD)
概念在开发之前,先编写单元测试用例代码,根据测试代码来明确开发需要实现的业务逻辑代码,注重测试软件。ATDD:验收测试驱动开发,在进行需求分析时就确定需求的验收标准。UTDD:单元测试驱动开发,在写业务代码前先写好功能的测试代码对ATDD的补充,通过编写行为和规范来驱动开发,注重设计软件关注Service层的设计,通过领域设计方法定义领域模型,从而确定业务和应用边界,保证业务模型和代码模型的一致性
优点①更好地促进代码的设计②更易维护①通过非技术语言扩大了受众,可读性强②聚焦系统行为①系统演进更方便,分为业务复杂型变化和业务数据量变化的演进②更方便测试
缺点①增加代码量,主要是需要编写一套测试代码②无法保证编写的测试用例一定是用户期望的功能系统改造成DDD复杂,开发熟悉DDD思想困难

4、前端测试框架

  1. jasmine:JavaScript测试框架(BDD:集成测试开发框架)
  2. MOCHA:本身不带断言库,需要和chai搭配使用
  3. Jest:目前最流行的前端测试框架,几乎国内大厂都在使用

二、Jest的优点

  1. 速度快:单独模块测试,比如A和B两个组件,之前测试过了,现在只改动A组件,再次测试,B组件不会再跑一次
  2. API简单
  3. 隔离性好:Jest中有很多测试文件可以使用,Jest的执行环境都是隔离的,避免不同的测试文件执行的时候互相影响造成出错
  4. IDE整合:Jest可以和很多编辑器(VSCode)进行融合,测试变得简单
  5. 多项目执行:比如写了Node.js的后台项目,用react写了一个前台项目,Jest支持并行运行
  6. 快速导出覆盖率(测试代码覆盖率):对于一个项目的测试都要出覆盖率的,Jest可以快速给出覆盖率统计结果

三、jest基本使用

1、jest环境搭建

  1. 创建JestTest文件夹,使用npm init -y初始化
  2. 安装jest:
  npm install jest@24.8.0 -D

2、创建业务逻辑文件和测试文件

  1. 根目录下创建utils.jsutils.test.js

utils.js:

const add = (a, b) => a + b;

const reduce = (a, b) => a - b;

const sum = (arr) => arr.reduce((acc, cur) => acc + cur, 0);

module.exports = { add, reduce, sum };

utils.test.js(也可以写成utils.spec.js):

const { add, sum } = require("./utils");

test("数字相加", () => {
  expect(add(1, 2)).toBe(3);
});

test("数组求和", () => {
  expect(sum([1, 2, 3])).toBe(6);
});

3、执行测试

  1. package.json中scripts中添加执行命令
    "jest": "jest"
  1. 执行npm run jest

image.png

四、Jest配置项

1、概念解释

  • 单元测试和集成测试

    • 单元测试(unit test):对软件中的最小可测试单元进行检查和验证。前端单元测试就是对一个模块进行测试。
    • 集成测试:也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照涉及要求组装称为子系统或系统,进行集成测试
  • 代码覆盖率(code coverage):测试代码功能性代码和业务代码的比重达到了百分之多少

2、Jest初始化配置

  1. 执行npx jest --init,生成jest.config.js

image.png 2. 执行npx jest --coverage,生成coverage文件夹

动图.gif 如果将coverageDirectory的值改wxm,那么就会生成wxm文件夹

五、Jest中的匹配器

根目录下新建jest.config.js

1、toBe()

test('toBe', () => {
  expect('帅逼').toBe('帅逼')
})

test('toBe是严格相等的', () => {
  // toBe()匹配器采用的是ES6的Object.is()比较,它基本和===一样
  expect(NaN).toBe(NaN) // 通过
  expect(+0).toBe(-0) // 不通过
  expect(2).toBe('2') // 不通过,和===一样
})

Object.is()比较+0和-0结果为false,比较两个NaN结果为true,这和===是不一样的

image.png

2、toEqual()

由于Object.is()比较两个对象时永远不相等,所以比较对象时使用toEqual()

test('toEqual', () => {
  expect({ name: 'xx', age: 10 }).toEqual({ name: 'xx', age: 10 }) // 通过
  expect({ name: 'xx', age: 10 }).toEqual({ name: 'xx', age: '10' }) // 不通过
})

3、toBeNull()

只匹配null值,不需要传参

test('toBeNull', () => {
  expect(null).toBeNull() // 可以使用 xpect(null).toBe(null) 替代
})

4、toBeUndefined()

只匹配undefined值,不需要传参

test('toBeUndefined', () => {
  expect(undefined).toBeUndefined() // 可以使用 expect(undefined).toBe(undefined) 替代
})

5、toBeDefined()

test('toBeDefined', () => {
  expect(false).toBeDefined() // 只要 expect 方法中有值并且不为 undefined 都可以通过
  expect(undefined).toBeDefined() // 不通过
})

6、toBeTruthy()

隐式转为为false的值不会通过,其他值都可以通过

test('toBeTruthy', () => {
  const a = 0 // 0 false NaN '' null undefined
  expect(a).toBeTruthy()
})

7、toBeFalsy()

隐式转换为false的值才会通过,和toBeTruthy()相反

test('toBeFalsy', () => {
  const a = 0 // 0 false NaN '' null undefined
  expect(a).toBeFalsy()
})

8、toBeGreaterThan()

test('toBeGreaterThan', () => {
  expect(10).toBeGreaterThan(9) // 大于,expect中的参数大于toBeGreaterThan中的参数
})

9、toBeLessThan()

test('toBeLessThan', () => {
  expect(10).toBeLessThan(11) // 小于
})

10、toBeGreaterThanOrEqual()

test('toBeGreaterThanOrEqual', () => {
  expect(10).toBeGreaterThanOrEqual(10) // 大于等于
})

11、toBeLessThanOrEqual()

test('toBeLessThanOrEqual', () => {
  expect(10).toBeLessThanOrEqual(10) // 小于等于
})

12、toBeCloseTo()

test('toBeCloseTo', () => {
  expect(0.1 + 0.2).toBeCloseTo(0.3) // 消除浮点精度丢失问题
  expect(0.1 + 0.2).toBe(0.3) // 不通过
  expect(0.1 + 0.2).toBe(0.30000000000000004) // 通过
})

13、toMatch()

test('toMatch', () => {
  expect('大帅比').toMatch('帅比') // 判断字符串是否包含
  expect('大帅比').toMatch(/帅比/) // 也可以使用正则
})

14、toContain()

test('toContain', () => {
  expect(['大帅比']).toContain('大帅比') // 判断数组中是否包含某项
  expect(new Set(['大帅比'])).toContain('大帅比') // 也可以兼容set
})

15、toThrow()

const fn = () => {
  throw new Error('fn抛出一个错误')
}

const fn1 = () => {
  const a = 0
  if (a) throw new Error('fn1抛出一个错误')
}

const fn2 = () => {
  throw new Error('fn2抛出一个错误')
}
test('toThrow', () => {
  expect(fn).toThrow() // 判断fn中会不会抛出异常,通过
  expect(fn1).toThrow() // 不通过
  expect(fn2).toThrow('fn3抛出一个错误') // 不通过,fn2中抛出的异常提示文字要包含toThrow中的文字才可以通过
})

16、not

const fn = () => {
  throw new Error('fn抛出一个错误')
}

test('not匹配器', () => {
  expect(fn).not.toThrow() // 判断fn中会不会抛出异常,如果抛出了异常则不通过,这里是不通过
})

test('not', () => {
  expect(100).not.toBe(101) // 通过
})

六、让Jest支持ES6的导入导出

Jest默认支持的是CommonJS规范,也就是Node.js中的语法,只支持module.export = {}/require()这种导入导出,可以使用Babel将代码转换为CommonJS代码

Jest中有一个babel-jest文件,当使用npm run jest时,先去检测开发环境中是否安装了babel,也就是查看有没有babel-core,如果有babel-core就会去查看.babelrc文件,根据配置文件进行转换

操作步骤:

  1. 安装babel
npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
  1. 项目根目录创建.babelrc文件
{ "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]] }
  1. 重启

可以在脚本中加上这一行,监听xxx.test.js文件,执行npm run jestWatch,有改动时自动执行测试用例

    "jestWatch": "jest --watchAll",
  1. 这样便可以使用ES6的导入了
import { add } from './utils'

七、异步代码的测试方法

安装axios

npm run axios@0.19.0

1、回调函数式

fetchData.js:

import axios from 'axios'

export const getList = async cb => {
  const url = 'https://cnodejs.org/api/v1/topics/?page=1&limit=10'
  const res = await axios.get(url)
  console.log('res>>>>', res.status)
  cb(res)
}

fetchData.test.js:

import { getList } from './fetchData'

test('接口测试-回调函数', done => {
  getList(res => {
    expect(res.status).toBe(200)
    done()
  })
})

2、直接返回promise

fetchData.js

export const getList = () => {
  const url = 'https://cnodejs.org/api/v1/topics/?page=1&limit=10'
  return axios.get(url)
}

fetchData.test.js

test('接口测试-返回promise', async () => {
  const res = await getList()
  expect(res.status).toBe(200)
})

八、Jest中的4个钩子函数

beforeAll(() => {
  console.log('所有用例之前执行')
})

beforeEach(() => {
  console.log('每个用例之前执行')
})
test('第一个测试', () => {
  expect(1).toBe(1)
})

test('第二个测试', () => {
  expect('2').toBe('2')
})

afterEach(() => {
  console.log('每个用例之后执行')
})

afterAll(() => {
  console.log('所有用例之后执行')
})

九、describe()

对测试用例进行分组

describe('分组1', () => {
  test('第一个测试', () => {
    expect(1).toBe(1)
  })
})

describe('分组2', () => {
  test('第二个测试', () => {
    expect('2').toBe('2')
  })
})

十、钩子函数的作用域

  1. 子级分组继承父级分组的钩子
  2. 不同分组下的钩子互不干扰
  3. 分组嵌套就像组件嵌套,先从外到内,再从内到外

1、子级分组继承父级分组的钩子

image.png

2、不同分组下的钩子互不干扰

image.png

3、分组嵌套就像组件嵌套,先从外到内,再从内到外

image.png

十一、only

当测试用例比较多时,只想调试某一个用例,就可以对当前用例加上only修饰,当前文件中其他所有用例就会被skipped

describe('一级分组', () => {
  test('第一个用例', () => {
    expect().toBe(undefined)
  })

  test.only('第二个用例', () => {
    expect().toBe(undefined)
  })

  test('第三个用例', () => {
    expect().toBe(undefined)
  })
})

image.png