单元测试梳理

883 阅读6分钟

在多人合作的大项目中,如果出现了bug,其他模块也受影响,去定位bug的位置和责任人需要花费非常多的时间,修改某个模块,可能引起整体主流程出现问题,对于组件,更需要单元测试,并且要达到一定程度的测试覆盖率

前端自动化测试产生的背景及原理

单元测试定义

  • 单元测试:对软件中的最小可测试单元进行测试
  • 最小可测试单元:方法(组成整体功能和整个系统的最小单元)

为什么要进行单元测试

  • 分模块开发的大项目,方便定位到哪个模块出了问题
  • 保证代码质量,各个开发对自己写的方法进行单元测试,降低出问题的概率
  • 由测试驱动开发,先写单元测试,再根据怎么测驱动开发

单元测试分类

  • TDD test driven development
    • 测试驱动开发,从需求角度出发
    • 需求分析(分析需要哪些方法) => 编写单元测试 => 编写功能代码使单元测试通过 => 重构
    • 当测试用例都通过后,相应的功能也就都做完了
    • 测试重点在于代码
    • 一般结合单元测试使用,是白盒测试
    • 优点
    • 很好的诠释了代码即文档
      • 清晰地了解软件的需求
      • 代码质量更好(组织,可维护性)
      • 测试覆盖率高
      • 错误测试代码不容易出现
      • TDD适合于对系统代码质量和测试覆盖率有要求的开发主体,比如函数和组件库
  • BDD behavior driven development
    • 行为驱动开发,从具体功能角度出发
    • 业务角度定义目标 => 找到实现目标的方法 => 编写单元测试 => 实现行为 => 检查产品
    • 在功能开发完成后,以用户的操作为指导编写测试代码
    • 测试重点在于UI(DOM)
    • 一般结合集成测试使用,是黑盒测试
    • 优点
    • 保证程序实现效果和用户需求一致
      • BDD的模式适用于平时的业务代码开发,因为业务的需求有可能变更频繁

测试原则

  • 根据需求变更及时修改和维护
  • code review
  • 只测单一的点,单一方法(不要有其它方法影响)

单元测试核心内容

  • 测试框架
  • 断言库
    • 用来判断结果
  • Mock库
    • 屏蔽掉方法中其它方法的影响
  • Test runner
    • 跑单元测试的环境
  • 覆盖率工具

单元测试常用库

常用测试框架

  • Jest
    • 基于jasmine,对react友好
  • Jasmine
    • BDD风格,已集成assert(断言库)和mock库
    • 全部集成
  • Mocha
    • 支持TDD和BDD
    • 不带assert和mock,需要自己搭配,可自定义
    • 全面适合node和浏览器两个端
  • Qunit
    • 出自jquery,后来独立出来(较古老)

常用的是 jestmocha+karma,最大区别在于是否全部集成了断言库和mocha库

断言库

  • Chai
    • 支持所有风格——全面
  • Assert
    • node环境下直接使用
  • Should
  • expect

最常用的是 Chai

Mock

  • sinon

Test runner

  • karma
    • 模拟的浏览器:无头浏览器,可以模拟用户在真实浏览器上的操作( puppeteer)

覆盖率工具

  • istanbu

在vue中使用单元测试

单元测试demo

  • 单元测试是针对最小可测试单元,如果复杂模块,需要进行拆分成单一函数再进行测试
function a() {
	return 2
}

function b() {
	div1.innerHTML = '123'
}
/**
 * @description: 对某个项目进行单元测试(总的)
 * @param { 
 *  param1:描述某个组件
 *  param2:测试用例的回调
 * } 
 * @return {type} 
 */
describe('this is a test for X', () => {
  /**
   * @description: it语法新建一个测试用例
   * @param { 
   *  param1:对这个测试用例的描述
   *  param2:测试用例执行方法的回调
   * } 
   * @return {type} 
   */
  it('test function a', () => {
    expect(a()).to.equal(2)
  })
  // 测试没有返回值的情况
  it('test function b', () => {
  	b()
    expect(div.innerHTML).to.equal('123')
  })
})

vue项目中编写单元测试

vue中的测试,在vue-cli搭建工具中推荐的两种测试是e2e(端到端测试)和unit test(单元测试)

  • e2e测试 end to end
    • 认为整个系统都是黑箱,只有UI会暴露给用户,一般是测试来测,与前端关系不大
  • 单元测试
    • 对一个模块,一个函数或者一个类来进行正确性检验的测试工作tzr

vue项目中选择karma+mocha

初始化后,运行npm run test,运行结果如下

import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
import { expect } from 'chai'

import axios from 'axios'
// 引入axios必需要引入的Promise polyfill
import Promise from 'es6-promise'
Promise.polyfill()

describe('HelloWorld.vue', () => {
  // Vue.extend拿到HelloWorld的构造函数
  const Constructor = Vue.extend(HelloWorld)
  // 新建这个组件的构造函数,也就是组件里的this
  const vm = new Constructor().$mount()

  // mocha的钩子函数
  // 所有用例执行前
  before(() => {
    console.log(1)
  })
  // 每个用例执行前
  beforeEach(() => {
    console.log(2)
  })
  // 所有用例执行后
  after(() => {
    console.log(3)
  })

  it('should render correct contents', () => {
    expect(vm.$el.querySelector('.hello h1').textContent)
      .to.equal('Welcome to Your Vue.js App')
  })
  it('is m1 add two arguments', () => {
    const m1 = vm.m1
    expect(m1(1, 2)).to.equal(3)
  })
  // 异步函数的用例,用回调
  it('async m2 should return', () => {
    const m2 = vm.m2
    m2(2, 2, (data) => {
      expect(data).to.equal(4)
    })
  })
  // 异步接口请求的用例
  it('接口请求', () => {
    const m3 = vm.m3
    // 3种方式
    // spy间谍函数,获取到调用信息,不会屏蔽
    // stub 屏蔽掉请求的方法

    // 先屏蔽掉实际请求
    let callback = sinon.stub(axios, 'get')
    // 再通过spy新建一个请求,模拟他的返回值
    let axiosSpy = sinon.spy(() => {
      return 4
    })
    // 检测拿到的结果是否正确
    expect(m3(axiosSpy)).to.equal(4)
    callback.restore()
  })
})

vue项目中选择jest

初始化后,运行npm r![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/122250bf832f4e5c8c2dc1e3eed70dd6~tplv-k3u1fbpfcp-zoom-1.image)![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b01cc60a2c343309bac1fd68146ac16~tplv-k3u1fbpfcp-zoom-1.image)un test,会报错,需要先在 jest.conf.js 文件中注释掉以下行 然后还有报错,需要安装依赖 npm i -D @vue/test-utils babel-jest jest jest-serializer-vue vue-jest 然后还有报错,网上查询解决方案 stackoverflow.com/questions/5… npm install --save-dev babel-jest@22.4.3

jest中的匹配器

jestjs.io/docs/zh-Han…

jest测试异步代码

接收回调的异步
// fetchData.js
export const fetchData = (fn) => {
	axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
    	fn(response.data)
    })
}

对应的单元测试

test('fetchData 返回结果为{success: true}', (done) => {
    fetchData().then(data => {
    	expect(data).toEqual({
        	success: true
        })
    	done() // 必须要有done才会测试这个异步函数的返回值,不然走完fetchData就算是测试通过了9
    })
})
return promise的异步
// fetchData.js
export const fetchData = () => {
	return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

对应的单元测试

// 方式1
test('fetchData 返回结果为404', () => {
	expect.assertions(1) // 至少执行一次(如果不加这行,就不会进入catch, 这个次数对应的是catch里的expect个数)
    return fetchData().catch(e => {
    	expect(e.toString().indexOf('404') > -1).toBe(true)
    })
})

// 方式2
test('fetchData 返回结果为404', async () => {
	// 除了async,await还可以用return 
    await expect(fetchData()).resolves.toMatchObject({
    	data: {
        	success: true
        }
    })
})

// 方式3 对于异常
test('fetchData 测试异常', () => {
	return expect(fetchData()).rejects.toThrow()
})

jest中的钩子函数

一个describe内部的钩子函数对内部所有函数都有效

  • beforeAll 所有测试用例执行之前
  • afterAll 所有测试用例执行完成之后
  • beforeEach
  • afterEach
  • describe 可以通过 describe 块来将测试分组。 当 before 和 after 的块在 describe 块内部时,则其只适用于该 describe 块内的测试

单元测试编写原则

前端自动化测试中,不会发送真正的请求,需要用mock请求