单元测试jest入门

单元测试

什么是单元测试?

  • 单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作,这里的单元是程序工作的最小工作单位,单元测试应该仅仅依赖输入,不依赖多余的环境.
为什么要写单元测试?
  1. 减少缺陷率
  2. 是很好的“文档”,代码重构的基础
什么时候写单元测试?
  1. TDD 测试驱动开发
  2. BDD 行为驱动开发
什么代码需要写单元测试?
  1. 逻辑复杂的
  2. 容易出错的
  3. 不易理解的
  4. 公共代码
  5. 核心业务功能
1. 单元测试框架:jest
  • jest的特点
  1. 易用性:基于Jasmine,提供断言库,支持多种测试风格
  2. 适应性:Jest是模块化、可扩展和可配置的
  3. 快速:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行
  4. 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测
  5. Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
  6. 支持异步代码测试:支持Promise和async/await
  7. 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告
  • 安装jest
  1. yarn add --dev jest/ npm install --dev jest

2. enzyme

  • jest+Enzyme 是目前比较流行的React项目单元测试组合
  • 是由 airbnb 开发的React单测工具,它模拟了jQuery的API
  • yarn add --dev enzyme enzyme-adapter-react-16
  • enzyme配合react16使用的初始化配置:
 // 使用enzyme还需要根据React的版本安装适配器
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({
  adapter: new Adapter()
});

jest的常用配置:

// https://jestjs.io/docs/zh-Hans/configuration#testmatch-array-string

"jest": {
    // 指定需要进行单元测试的文件匹配规则
    "testMatch": ["<rootDir>/src/**/*.test.js"],
    // 测试启动文件
    "setupFiles": [
      "<rootDir>/setup/jest.setup.js",
      "<rootDir>/setup/enzyme.setup.js"
    ],
    // 单元测试文件检测后缀名
    "moduleFileExtensions": ['js', 'jsx'], 
    // 需要忽略的文件匹配规则
    "testPathIgnorePatterns": ["src/node_modules/"],
    "collectCoverage": false,
    //设置阈值
    "coverageThreshold": {
      "global": {
        "statements": 60,
        "functions": 50,
        "branches": 40
      }
    },
    "coverageDirectory": "<rootDir>/coverage",
    "coverageReporters": [
      "cobertura",
      "html",
      "lcov"
    ],
    //层次显示测试套件中每个测试的结果。
    "verbose": false
  },

运行:npm run test / yarn test

3. 测试基本分为三步

  1. describe: 定义一个测试套件
  2. test/it:定义一个测试用例
  3. expect:断言的判断条件 toEqual:断言的比较结果

4. 钩子函数

  • beforeAll() 会在所有测试用例之前执行一次
  • afterAll() 会在所有测试用例之后执行一次
  • beforeEach() 会在每个测试用例之前执行
  • afterEach() 会在每个测试用例之后执行
  • 注:describe的after函数优先级要高于全局的after函数,describe的before函数优先级要低于全局的before函数

5. jest对象

  • jest.fn() 创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

  • jest.spyOn() 返回一个mockfunction,和jest.fn相似,但是能够追踪调用信息

  • jest.mock() 用来mock一些模块或者文件

jest.mock('src/index', () => ({
  getStore: jest.fn(() => ({
    getState: jest.fn(() => ({})),
    dispatch: jest.fn(),
  })),
}))

jest.mock('src/xxx/xx, () => ({
  show: jest.fn(),
  showShortCenter: jest.fn(),
}))
* 总结
  • jest.fn()常被用来进行某些有回调函数的测试
  • jest.mock()可以mock整个模块中的方法,节约测试时间和测试的冗余度是十分必要
  • jest.spyOn()需要测试某些必须被完整执行的方法时

6. 快照测试

  • 需要引入react-test-renderer库,使用其中的renderer方法
    快照可以测试到组件的渲染结果是否与上一次生成的快照一致;
    toMatchSnapshot方法会帮助我们对比这次将要生成的结构与上次的区别;

安装: npm i -D babel-preset-react react-test-renderer

  • 例子
//例如:
  render() {
    const { width, height, direction } = this.props

    if (direction === 'horizontal') {
      return <View style={[{ backgroundColor: this.props.lineColor, height }, this.props.style]} />
    }
    return <View style={{ backgroundColor: this.props.lineColor, width }} />
  }
//快照test文件
import React from 'react'
import renderer from 'react-test-renderer'
import Line from './Line'

describe('Line snapshot', () => {
  test('should render Line correctly', () => {
    const tree = renderer.create(<Line />).toJSON()

    expect(tree).toMatchSnapshot()
  })

  test('should render Line correctly when direction is vertical', () => {
    const tree = renderer.create(<Line direction="vertical" width={3} />).toJSON()

    expect(tree).toMatchSnapshot()
  })

  test('should render Line correctly when direction is horizontal', () => {
    const tree = renderer.create(<Line direction="horizontal" height={3} />).toJSON()

    expect(tree).toMatchSnapshot()
  })
})

7. 异步测试

  • jest支持对异步的测试,支持Promise和Async/Await两种方式的异步测试

8. 常用的断言库

  • toBe(value): 比较数字、字符串
  • toEqual(value): 比较对象、数组
  • toBeNull(): 只匹配null
  • toBeUndefined(): 只匹配undefined
  • toBeDefined(value):与toBeUndefined相反
  • toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
  • not:用来取反
  • toHaveLength(): 有多少个
用来判断mock function是否被调用过
  • toHaveBeenCalledWith()
  • toHaveBeenCalled()

9. enzyme的渲染方法

  • shallow 浅渲染,只渲染当前组件并将组件渲染成虚拟DOM对象,只能能对当前组件做断言
  • render 静态渲染,渲染成静态的HTML字符串
  • mount 完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期,当前组件以及所有子组件,耗时更长,内存占用的更多
常用方法
  1. simulate(event,mock):模拟事件,用来触发事件,event为事件名称,mock为一个event object
  2. instance():返回组件的实例
  3. find(selector):根据选择器查找节点,selector可以是CSS中的选择器,或者是组件的构造函数,组件的display name等
  4. at(index):返回一个渲染过的对象
  5. get(index):返回一个react node,要测试它,需要重新渲染
  6. contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
  7. text():返回当前组件的文本内容
  8. html(): 返回当前组件的HTML代码形式
  9. props():返回根组件的所有属性
  10. state():返回根组件的状态
  11. setState(nextState):设置根组件的状态
  12. setProps(nextProps):设置根组件的属性

10. 组件测试

  • 工具类函数测试
export const isHPurchase = (type) => {
  return type === ‘HPurchase’
}
//test文件
describe('test type is isHPurchase', () => {
    test.each([
      ['HPurchase', true],
      ['h_REDEMPTION', false],
      ['', false],
      [null, false],
      [undefined, false],
      [11, false],
      ['PURCHASE', false],
    ])('should return %s when type is %s', (type, expected) => {
      expect(isHPurchase(type)).toEqual(expected)
    })
 })
  • 组件类测试
// 组件
render() {
    const { heightStyle, noBottomBorder } = this.props
    return (
      <TouchableWithoutFeedback onPress={this.handleOnClick}>
        <View style={styles.container}>
          <View style={[styles.ItemView, heightStyle.height !== null && heightStyle]}>
            <View style={styles.left}>{this.renderLeft()}</View>
            <View style={{ flex: 1, marginRight: 10 }}>{this.renderRight()}</View>
          </View>
          {!noBottomBorder && <View style={[styles.line, { marginLeft: 20 }]} />}
        </View>
      </TouchableWithoutFeedback>
    )
  }
//测试文件
import { shallow } from 'enzyme'
import { View, TouchableWithoutFeedback } from 'react-native'
import Toast from 'src/modules/Toast'

import { UserInfoItem } from './UserInfoItem'

describe('UserInfoItem', () => {
  test('UserInfoItem', () => {
    const renderRight = jest.fn()
    const renderLeft = jest.fn()
    const props = {
      isEditable: false,
      noBottomBorder: true,
      renderRight,
      renderLeft,
    }

    const component = shallow(<UserInfoItem {...props} />)
    component.find(TouchableWithoutFeedback).simulate('press')

    expect(component.find(TouchableWithoutFeedback)).toHaveLength(1)
    expect(Toast.showShortCenter).toBeCalledWith('该信息不可编辑')
    expect(component.find(View)).toHaveLength(4)
  })
})

11. 代码覆盖率

  • 行覆盖率(line coverage):是否测试用例的每一行都执行了
  • 函数覆盖率(function coverage):是否测试用例的每一个函数都调用了
  • 分支覆盖率(branch coverage):是否测试用例的每个if代码块都执行了
  • 语句覆盖率(statement coverage):是否测试用例的每个语句都执行了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值