浅谈前端测试

发表于:2019-5-07 11:57

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:orange    来源:思否

  前端测试或许被好多人误解,也许大家更加倾向于编写面向后端的测试,逻辑性强,测试方便等
  聊到这导致了好多前端从来不写测试(测试全靠手点~~~)
  其实没必要达到测试驱动开发的程度,只要写完代码可以补测试,并且补出高效的测试,前端或许真的不需要手点
  大前端时代不谈环境不成方圆,本文从下面几个环境一一分析下如何敏捷测试
  node 环境
  vue 环境
  nuxt 服务端渲染环境
  react 环境
  next 服务端渲染环境
  angular 环境
  理解测试前需要补充下单元测试(unit)和端到端测试(e2e)的概念,这里不赘述
  node 环境
  推荐测试框架 jest
  jest 是 FB 的杰作之一,方便各种场景的 js 代码测试,这里选择 jest 是因为确实方便
  使用方法及配置信息可以去官方文档
  配置的注意事项
   {
  testEnvironment: 'node' // 如不声明默认浏览器环境
  }
  针对 node 只聊一下单元测试,e2e 测试比较少见
  当决定写一个 npm 模块时,代码完成后必不可少的就是单元测试,单元测试需要注意的问题比较琐碎
  mock
  当引入三方库时,不得不 mock 数据,因为单元测试更多讲求的是局部测试,不要受外界三方引入包的影响
  例如:
   const { readFileSync } = require('fs')
  const getFile = () => {
  try {
  const text = readFileSync('text.txt', 'utf8')
  } catch (err) {
  throw new Error(err)
  }
  console.log(text)
  }
  module.exports = getFile
  这时我们并不需要关心 text.txt 是否真的存在,也不需要关系 text 的内容具体是什么,我们的关注点应该在于读取文件错误时能否及时抛出异常,以及 console.log() 是否如预期执行
  对应到测试
   const getFile = require('./getFile')
  describe('readFile', () => {
  const mocks = {
  fs: {
  readFileSync: jest.fn()
  },
  other: {
  text: 'Test text'
  }
  }
  beforeAll(() => {
  jest.mock('fs', () => mocks.fs)
  })
  test('read file success run console.log', () => {
  mocks.fs.readFileSync.mockImplementation(() => this.mocks.other.text)
  getFile()
  expect(console.log).toBeCalled()
  })
  })
  上面代码简单的实现了一个读取文件是否成功的测试,先别急着纠错,这段测试本身是错的,下面慢慢分析
  我们在最开始创建了一个 mocks 对象,用来模拟数据,由于 readFileSync 方法可能存在多种返回结果(成功或报错),所以暂时用 jest.fn() 模拟
  other 里面则是放一些固定的测试数据(不会随着测试过程而改变)
  beforeAll 钩子里面执行我们的 mock,把 require 进来的 fs 模块拦截调,也是本测试用例中的关键步骤
  在第一个 test 里面我们改写 mocks.fs.readFileSync 的返回形式,这里使用的 mockImplementation 是直接模拟了一个执行函数,当然也可以模拟返回值,具体可以到 jest 官网
  expect 用来断言我们的 console.log 方法执行了
  解释了这么多测试新手们应该也都看的明白了,下面聊一下错在哪,怎么改进
  1.mockImplementation 最好替换为 mockReturnValueOnce,注意这里出现了 Once 结尾,也就是仅模拟一次返回值,mockImplementation 最好使用在复杂场景,所谓的复杂就是我们手动实现一个 readFileSync 方法使得测试达到我们预期的目的,在这个简单的场景里面我们只需要模拟返回值就好
  2.expect(console.log) 这里会报错,因为 jest 断言的内容只能是 mock function 或 spy,这里 console 是全局对象 global 上的方法,我们没有 require 将其引入,所以 jest.mock 显然处理上有些吃力,这时候 spy 就派上用场了,beforeAll 钩子里直接执行 jest.spyOn(global.console, 'log'),接下来我们就能监听到 console.log 的执行了 expect(global.console.log)
  3.断言的目的是测试 console.log 的执行,这是不严谨的测试,我们需要使用 toBeCalledWith 来代替 toBeCalled,不仅要测试执行了,而且要测试参数正确,简单修改为 expect(global.console.log).toBeCalledWith(this.mocks.other.text)
  下面补一下 read file 失败的测试
  test('read file fail throw error', () => {
  mocks.fs.readFileSync.mockImplementationOnce(() => { throw new Error('readFile error') })
  expect(getFile()).toThrow()
  expect(global.console.log).not.toBeCalled()
  })
   读取文件失败的测试就好理解的多,注意的就是对一个 jest.fn() 多次进行修改会导致测试用例之间的相互影响,这里尽量使用 Once 结尾方法,复杂场景可以如下
   beforeEach(() => {
  mocks.fs.readFileSync.mockReset()
  })
  每次执行 test 前先清除 mock,避免多个测试用例之间复杂化 mock 导致错误
  小结:单元测试中的 mock 是个测试思路,我们无需关心外部文件和依赖是什么,只要能模拟出正确的情况程序是否按规则执行,错误的情况程序是否有异常处理,逻辑是否正确等。这样就能排除外界干扰,使得我们测试的当前一小部分是可靠的,稳定的即可。
  引用外部文件
  单拿出一个小结说下 require 的问题,node 9 之前不支持 es6 的 import,这里也不详细说明了。
  require 本身并不复杂,但是如果搞不清楚执行时机,那么测试将无法进行,来一个例子
   const env = process.env.NODE_ENV
  module.export = () => env
  测试如下
   const getEnv = require('./getEnv')
  describe('env', () => {
  test('env will be dev', () => {
  process.env.NODE_ENV = 'dev'
  expect(getEnv()).toBe('dev')
  })
  test('env will be pord', () => {
  process.env.NODE_ENV = 'pord'
  expect(getEnv()).toBe('pord')
  })
  })
  十分简单的测试,抛开了 mock 的流程,这里会报测试未通过,原因是 require 同时 env 已经被赋值为 undefined,我们再试着改变 NODE_ENV 环境变量时,程序不会再次执行,当然了,处理起来也十分简单
   let getEnv
  test('env will be dev', () => {
  process.env.NODE_ENV = 'dev'
  getEnv = require('./getEnv')
  expect(getEnv()).toBe('dev')
  })
  test('env will be pord', () => {
  process.env.NODE_ENV = 'pord'
  getEnv = require('./getEnv')
  expect(getEnv()).toBe('pord')
  })
  顺带说了一下,希望大家不要在这种低级错误上浪费时间
  其实引用外部文件还有些场景会对测试带来困惑,比如动态路径,场景如下
   const packageFile = `${process.cwd()}/package.json`
  const package = require(packageFile)
  读取当前路径下的 package.json,当测试真正跑到这段代码时会到当前目录下找 package.json,这里尽量 mock 掉 package.json 为我们自己的模拟数据,但是 jest 不支持动态路径的 mock,试着这样写 jest.mock(${process.cwd()}/package.json, () => mockFile) 会报错,所以尽量使用可以 mock 的方案,保证单元测试可以顺利进行,修改如下
   const path = require('path')
  const filePath = path.join(process.cwd(), 'package.json')
  这样就可以 mock,path 了,和上面 mock 章节,大致思想都差不多
  覆盖率
  单元测试覆盖率不达标等于白测,测试过程尽量覆盖所有判断条件,而不是全部通过了就不管了,在进一阶说,100% 的测试覆盖率并不证明一定覆盖到位了,因为顺带执行的代码也会算进覆盖率,例如
 module.export = (list) => list.map(({ id }) => id)
  我们先不考虑这个 list 类型是不是数组,只是简单的例子,避免过度设计带来复杂化,我们测试可以这样
   const getId = require('./getId')
  const mocks = {
  list: [{
  id: 1,
  name: 'vue'
  }, {
  id: 2,
  name: 'react'
  }]
  }
  test('return id', () => {
  expect(getId(mocks.list)).toEqual([1, 2])
  })
  直到有一天代码变成了 module.export = (list) => [1, 2]
  这时候测试还能通过,并且覆盖率 100%,的确不会有人蠢到把代码改成这样,只是一个例子,实际上逻辑会比这个复杂的多
  那就聊一聊解决方案
  mock 数据的随机化,每次测试生成随机的 list 进行测试,现有库 mockjs
  强关联测试,证明 map 方法的确执行了,并且参数正确,先 spy spyOn(Array.prototype, 'map') 然后断言
  聊了一圈从覆盖率聊到了测试健壮性的问题,可以思考下写过的测试是否真的满足注释或修改任何一行代码都能引起测试的 pass 报错
  关于 node 就聊这么多,其实下文主要思想都一样,更多的是介绍些简单可行的方案,以及可能会踩坑的地方

     上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
  
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号