这篇文章主要向大家介绍前端自动化测试详解,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

1 前言

文章研究了四个问题:什么是自动化测试、为何要自动化测试、什么项目适合自动化测试、自动化测试具体要怎么作。在寻找这四个问题答案的过程当中,梳理了一套完整的前端自动化测试方案,包括:单元测试接口测试功能测试基准测试

2 什么是自动化测试

维基百科是这样定义的html

在软件测试中,测试自动化(英语:Test automation)是一种测试方法,使用特定的软件,去控制测试流程,并比较实际的结果与预期结果之间的差别。经过将测试自动化,可让正式的测试过程当中的必要测试,能够反复进行;经过这种方法,也能够将难以手动进行的测试,交由软件来作。前端

测试自动化的最大优点就是能够快速并且反复的进行测试。java

总结一下:自动化测试指软件测试的自动化,让软件代替人工测试,能够快速、反复进行。node

关于自动化测试有一个金字塔理论,把测试从上到下分为UI(用户界面测试)/Service(服务测试) /Unit(单元测试 )。如图所示,越往金字塔底层,测试的效率越高,测试质量保障程度越高,测试的成本越低。怎么理解这句话呢?前端项目一般UI变化频繁,一旦发生变化,UI测试用例就没法执行且难以维护,因此UI自动化测试的成本高,收益小;相比UI测试,Service测试更加简单直接且变化不会很频繁;单元测试主要对公共函数、方法进行测试,测试用例复用度高且更能保证代码质量。

 

在接下来的问题中,咱们所讨论的自动化测试,主要指四个方向: 单元测试接口测试功能测试基准测试。所谓单元,能够理解为一个函数、一个react组件;接口即API,接口测试主要关注提供的接口是否可靠;功能能够理解为应用的UI、功能是否符合预期;基准测试能够帮咱们测试代码的性能。

3 实施自动化测试有什么好处

测试最重要的目的是验证代码正确性,确保项目质量。举个例子,某一天我写了一个逻辑复杂的函数,这个函数被不少地方调用,过了一个月以后,我可能忘记这里面的具体逻辑了,出于某种缘由须要为这个函数增长一些功能,修改这个函数的代码,那我要怎么作才能保证修改代码后不影响其余的调用者呢,或者说,我要怎么作,才能快速的知道哪些地方受影响,哪些地方不受影响呢?答案就是实施自动化测试,跑测试用例。git

若是不进行自动化测试,咱们会如何验证代码的正确性?一般FE使用的方法是手动测试:console、alert、打断点、点点点。但手动测试是一次性的,若是下次有人对代码功能作了修改,咱们不得再也不次重复手动测试的工做,而且很难保证测试的全覆盖。但若是编写测试用例进行自动化测试,第一次写完的测试用例是能够重复使用的,一次编写,屡次运行。若是测试用例写的完善、语义化,开发人员还能够经过看测试用例快速了解项目需求。实施自动化测试能够驱动开发人员在代码的设计中作更好的抽象,写可测试的代码,以测试公用方法为例,要确保被测试的方法无反作用,既对外部变量没有依赖,也不会改变全局本来的状态。github

总结一下,实施自动化测试有四个好处:web

  • 能够验证代码正确性,保证项目质量正则表达式

  • 测试用例能够复用,一次编写,屡次运行

  • 经过看测试用例能够快速了解需求

  • 驱动开发,指导设计,保证写的代码可测试

4 什么样的项目适合自动化测试

自动化测试如此优秀,那是否是全部项目都适合进行自动化测试?答案是否认的,由于有成本。在实施自动化测试以前须要对软件开发过程进行分析,基于投入产出来判断是否适合实施自动化测试。实施自动化测试的项目一般须要同时知足如下条件:

  • 需求变更不频繁
  • 项目周期足够长
  • 自动化测试脚本可重复使用
  • 代码规范可测试

若是需求变更过于频繁,维护测试脚本的成本过高;若是项目周期比较短,没有足够的时间去支持自动化测试的过程;若是测试脚本重复使用率低,耗费的精力大于创造的价值,不值得;若是代码不规范,可测试性差,那自动化测试实施起来会比较困难。

5 自动化测试怎么作

5.1 原始的测试方法

举个例子,如今有一个方法sum

const sum = (a, b) => { return a + b }

 如何证实sum方法的正确性?咱们一般会使用以下代码进行测试

// test/util.test.js
const sum = (a, b) => { return a + b }
if(sum(1,1)===2){
    console.log('sum(1,1)===2,测试结果符合预期,方法正确')
}else{
    console.log('sum(1,1)===2,测试结果不符合预期,方法出错')
}

执行测试代码后控制台输出结果以下:

 测试结果正确。假设如今把sum方法改成+1:

const sum = (a, b) => { return a + b + 1 }

这个输出虽然显示了方法出错的提示,可是对结果正确与错误没有作明显的区分,测试结论不够直观,咱们把测试代码修改一下

// test/util.test.js
const sum = (a, b) => { return a + b + 1 }
if (sum(1, 1) === 2) {
  console.log('    sum(1,1)===2,测试结果符合预期,方法正确')
} else {
  throw new Error('sum(1,1)===2,测试结果不符合预期,方法出错')
}

 这段代码执行后,一旦方法执行的结果不符合预期就主动抛出错误

这样就能更直观的看出测试结论。咱们进一步优化,使用nodejs提供的断言模块来书写测试用例

const sum = (a, b) => { return a + b + 1 }
const assert = require('assert')
assert.equal(sum(1, 1), 2)

执行测试代码后控制台结果以下

 

输出信息与刚才的效果相似:执行结果不符合预期就主动抛出错误。使用assert达到了相同的效果,但代码量减少了,而且更加语义化。

5.2 使用测试框架

上面的方法能够帮助咱们完成代码测试,那有没有更好的方式呢?咱们开发项目时一般会选择使用框架和库,使用框架的好处是约束咱们代码的风格,保证代码的可维护性和扩展性,使用工具库能够提升开发效率。同理,在实施自动化测试时咱们也会选择使用测试框架和库。目前市面上比较流行的前端测试框架有Mocha、QUnit、Jasmine、Jest等,以下作个简单介绍

框架能够为咱们输出更加直观的测试报告,好比像下面这样,正确和错误的测试结果都给咱们展现

还能够输出文档结构的测试报告,好比下面这样

 

5.3 测试方案技术选型

本文讨论的自动化测试方案技术选型以下:

  • 测试框架:mocha
  • 断言库:chai
  • 测试报告:mochawesome
  • 测试覆盖率:Istanbul
  • 测试浏览器:chrome
  • 浏览器驱动:selenium-webdriver/chrome
  • 接口测试http请求断言:supertest
  • react组件测试:enzyme
  • 基准测试:benchmark

选择Mocha是由于它:

  • 精简而灵活,扩展性强
  • 社区成熟用的人多
  • 各类测试用例在社区都能找到

下面咱们经过一段测试用例来看一下Mocha有什么能力:

能够看到Mocha最核心的四项能力

  • 测试用例分组
  • 生命周期钩子
  • 兼容不一样风格断言
  • 同步异步测试架构

代码中describe块称为“测试套件”,表示一组相关的测试,它是一个函数,第一个参数是测试套件的名称("测试 sum 方法"),第二个参数是实际执行的函数,分组让测试用例代码结构化,易于维护。it块称为"测试用例",表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1 加 1 应该等于 2"),第二个参数是实际执行的函数。

选择chai做为断言库是由于它提供了两种风格的断言:BDD风格(行为驱动开发)和TDD风格(测试驱动开发),其中BDD风格更接近天然语言。使用它能够自由、灵活的与Mocha搭配,下图是chai官网展现的两种断言风格。

5.4 测试方案代码

下面开始梳理完整的自动化测试方案,总体目录结构以下:

5.4.1 单元测试

(1)对以下方法进行单元测试

// /src/client/common/js/testUtil.js
export const sum = (a, b) => {
  return a + b
}

 编写好测试用例

import { sum } from '../../src/client/common/js/testUtil.js'
const { expect } = require('chai')

describe('单元测试: sum (a, b)', function () {
  it('1+1 应该等于 2', function () {
    expect(sum(1, 1)).to.be.equal(2)
  })
})
// skip能够指定跳过某个分组
describe.skip('单元测试:金额按千分位逗号分隔的方法 formatMoney (s, type)', function () {...})

 而后使用mocha执行测试用例,输出结果以下

 能够看到两个测试分组有一个测试经过,一个被咱们主动跳过。使用mocha执行测试用例时,由于咱们指定了测试报告格式--reporter参数为mochawesome,测试报告会被输出为以下的html格式

为了分析当前测试用例对源代码的覆盖状况,咱们使用Istanbul生成测试覆盖率报告

 代码覆盖率有四个测量维度:

  • 语句覆盖率(statement coverage):是否每一个语句都执行了
  • 分支覆盖率(branch coverage):是否每一个if代码块都执行了
  • 函数覆盖率(function coverage):是否每一个函数都调用了
  • 行覆盖率(line coverage):是否每一行都执行了

分别对应上图的Statements、Branches、Functions、Lines,点击左侧连接能够查看源码测试详情,绿色部分表示已被测试覆盖

 

关于测试覆盖率,须要强调的是,咱们不该该把测试覆盖率的高低做为检验项目质量的标准,只能做为参考。代码覆盖率真正的意义在于帮助开发者找到代码设计的问题,帮助咱们发现为何有的代码没有被测试覆盖到,是代码设计有问题,仍是加入了无用代码,它能够指导咱们在代码设计中作更好的抽象,写可测试的代码。

(2)React组件测试

如今有以下的React组件

// /src/client/components/Empty/index.jsx'
import React, { Component } from 'react'
import { Icon } from 'antd'

const Empty = (props) => {
  const placeholder = props.placeholder

  return (
    <div>
      <Icon type='meh-o' />
      <span>{placeholder || '数据为空'}</span>
    </div>
  )
}

module.exports = Empty

编写测试用例对它进行测试

import React from 'react'
import { expect } from 'chai'
import Enzyme, { mount, render, shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-15.4' // 根据React的版本安装适配器
import Empty from '../../src/client/components/Empty/index.jsx'
import { spy } from 'sinon' // 对原有的函数进行封装并进行监听

Enzyme.configure({ adapter: new Adapter() }) // 使用Enzyme 先适配React对应的版本

describe('测试React组件: <Empty />', () => {
  it('不传入属性时,组件中span的文本为"数据为空"', () => {
    const wrapper = render(<Empty />)
    expect(wrapper.find('span').text()).to.equal('数据为空')
  })

  it('传入属性"我是占位文本"时,组件中span的文本为"我是占位文本"', () => {
    const wrapper = render(<Empty placeholder='我是占位文本' />)
    expect(wrapper.find('span').text()).to.equal('我是占位文本')
  })
})

使用mocha执行测试用例会生成以下测试报告,测试经过

 测试覆盖率报告以下

5.4.2 接口测试

编写测试用例,使用supertest实施接口测试

const request = require('supertest')
const { expect } = require('chai')
const BASE_URL = 'http://127.0.0.1:1990'

describe('接口测试:商户登陆测试用例', function () {
  it('登陆接口 /api/user/login', function (done) {
    request(BASE_URL)
      .post('/api/user/login')
      .set('Content-Type', 'application/json') // set header内容
      .send({ // send body内容
        user_code: 666666,
        password: 666666
      })
      .expect(200) // 断言但愿获得返回http状态码
      .end(function (err, res) {
        // console.info(res.body) // 返回结果
        expect(res.body).to.be.an('object')
        expect(res.body.data.user_name).to.equal('商户AAAAA')
        done()
      })
  })
})

 执行接口测试用例生成以下测试报告

5.4.3 e2e测试

编写e2e测试用例,使用selenium-webdriver驱动浏览器进行功能测试

const { expect } = require('chai')
const { Builder, By, Key, until } = require('selenium-webdriver')
const chromeDriver = require('selenium-webdriver/chrome')
const assert = require('assert')

describe('e2e测试:商户系统端到端测试用例', () => {
  let driver
  before(function () {
    // 在本区块的全部测试用例以前执行
    driver = new Builder()
      .forBrowser('chrome')
      // 设置无界面测试
      // .setChromeOptions(new chromeDriver.Options().addArguments(['headless']))
      .build()
  })

  describe.skip('登陆相关传统用例-跳过', function () {...})

  describe('登陆商户系统', function () {
    this.timeout(50000)
    it('登陆跳转', async () => {
      await driver.get('http://dev.company.home.ke.com:1990/login') // 打开商户登陆页面
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[1]/input')).sendKeys(666666) // 输入用户名
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[2]/input')).sendKeys(666666) // 输入密码
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/div/button')).click() // 点击登陆按钮
      const currentTitle = await driver.getTitle()
      await driver.sleep(2000)
      expect(currentTitle).to.equal('商户管理系统')
    })
  })

  after(() => {
    // 在本区块的全部测试用例以后执行
    driver.quit()
  })
})

 使用mocha执行e2e测试用例生成以下测试报告

 下图是selenium-webdriver驱动chrome浏览器自动运行,进行功能测试

5.4.4 基准测试

假设当前须要测试正则表达式的test方法和字符串的indexOf方法的性能,咱们一般会采用以下方法进行测试:让两个方法分别执行1000次,比较哪一个耗时长。

// 判断某个字符串中是否存在特定字符,比较reg.test和str.indexOf性能
const testPerf = (count) => {
  var now = new Date() - 1
  var i = count
  while (i--) {
    /o/.test('Hello World!')
  }
  console.log(`test方法执行${count}次用时`, new Date() - 1 - now)
}

const indexOfPerf = (count) => {
  var now = new Date() - 1
  var i = count
  while (i--) {
    'Hello World!'.indexOf('o') > -1
  }
  console.log(`indexOf方法执行${count}次用时`, new Date() - 1 - now)
}

testPerf(1000)
indexOfPerf(1000)

 测试结果以下,由于代码执行较快,两个方法执行1000次的时间都为零,没法准确判断代码执行效率

科学的统计方法是须要屡次执行,对大量的执行结果进行采样,咱们可使用工具帮咱们完成这件事,以下使用benchmark进行测试

// 判断某个字符串中是否存在特定字符,比较reg.test和str.indexOf性能
const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()

// add test
suite.add('正则表达式test方法', function () {
  /o/.test('Hello World!')
})
  .add('字符串indexOf方法', function () {
    'Hello World!'.indexOf('o') > -1
  })
  // add listeners
  .on('cycle', function (event) {
    console.log(String(event.target))
  })
  .on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'))
  })
  // run async
  .run({ 'async': true })

 执行测试代码,结果以下,indexOf每秒执行的次数比test每秒执行的次数超出了一个数量级,因此indexOf性能更好

6 总结

梳理完单元测试、接口测试、功能测试、基准测试的具体实施方案后,结合自动化测试的特色咱们能够得出如下结论:

前端要不要进行自动化测试,须要根据具体的项目特色进行判断,对于知足如下条件的代码能够进行自动化测试:

  • 核心功能模块、函数
  • 短时间不会发生变化的UI组件
  • 提供外部调用的接口
  • 对方法性能进行基准测试

最后,要强调一点,咱们的目标是保证代码健壮、可维护,提升开发效率,自动化测试只是一种手段。

7 参考资料

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐