前端测试:Part II (单元测试)

原文:Testing Your Frontend Code: Part II (Unit Testing)

By Gil Tayar

单元测试

我们在Part1里已经说过,但与那测试就是测试单元的代码,不管这些单元是函数、模块还是类。多数人认为测试应该以单测为主,但我不这么认为,如果你同意也没有问题。我会一遍一遍又一遍地在这一系列文章中强调,你怎测试都行,只要你写了足够多的测试,让你对你的上线有信心就行。

不管你写多少单测,单测确实是最好写也最好懂的测试,它们天生具有函数属性。设定一个单元的输入,执行,检查输出(输入可能就是一个函数的参数,输出就是返回值)。

更重要的是你应该提醒自己写代码时要让这些单元彼此隔离,不会相互依赖,这样才能方便单测。

计算器应用中的单元

理论已经够多了,我们看下我们的计算器,源码在这里。这是个React应用,有两个主要的组件,keypaddisplay。显然他们是单元且对其他单元无依赖。但是他们是React层面的单元,以后我会专门讲如何测它们。

如果你已经阅读过代码可能会疑问为啥我不用JSX。原因是我不想使用代码转译。Node和现代的浏览器能够识别ES6,所以为啥不让你写的代码直接运行呢?是的,我的代码不能再IE中运行,但是这只是个demo,所以没问题。在一个真实的项目里,我会添加代码转译的。

译注:最新的代码仓库已经用JSX重构并增加了babel转译,所以这段呵呵了。

总有些代码负责处理当点击了一个数字或者运算符后的运算,哪部分代码完成这部分工作呢?按照现在时髦的做法,我也把我的组件分成了展示型组件(keypaddisplay)和状态型组件(calculator-app)。后一个组件时唯一一个有状态的组件,负责调用计算逻辑,并驱动display展示点击后应该显示啥。

calculator模块

负责计算的逻辑并不在组件中,它是一个单独的模块calculator,并不依赖React。这样的模块是最适合单测的。对UI和IO没有依赖的模块最适合单测。你应该尽力让你更多的业务逻辑以不依赖IO或者UI的形式写成模块。

在前端里,不依赖IO是啥意思呢?不访问文件、数据库… ?不,前端里本来就没有这些。但是还会有Ajax,local storage,DOM 访问,浏览器API访问,对我而言,这些都是IO。

我是如何把计算器的逻辑和组件分开的?在这里,其实很简单,这些逻辑都是算法类的,我把它们放到了calculator模块中。

这个模块非常简单,输入是计算器的当前状态(一个对象)和一个字符(一个数字或者运算符),返回值是计算器的新状态。如果你用过Redux,这个逻辑跟Redux的Reducer差不多。但是如何获取最原始的状态呢?简单,这个模块同样Export了initialState,你可以用它去初始化计算器。计算器的状态并非不透明的,它包含可一个组件display,用来将计算器的内在状态显示出来。

如果你没有耐心去仔细阅读代码,我们这里只看下开头就行,这部分最重要,算法是怎么实现的其实无关紧要。

module.exports.initialState = { display: '0', initial: true }

module.exports.nextState = (calculatorState, character) => {
  if (isDigit(character)) {
    return addDigit(calculatorState, character)
  } else if (isOperator(character)) {
    return addOperator(calculatorState, character)
  } else if (isEqualSign(character)) {
    return compute(calculatorState)
  } else {
    return calculatorState
  }
}

//....
复制代码

怎么测试呢?我们通常会用一个测试框架。当下最流行的测试框架是Mocha,我们就用它来测试。当然用Jest,Jasmine,Tape或者其他框架都可以。

用Mocha进行单测

所有的测试框架都是类似的——你把测试代码写成函数,测试框架负责执行它们。

npm installMoch后,我们就可以通过npm脚本运行了。当然,命令就是"Mocha"。看下package.json,你会看到:

"scripts": {
...
    "test": "mocha 'test/**/test-*.js' && eslint test lib",
...
},
复制代码

运行npm test,就会执行以test打头的文件夹里的测试脚本。如果你clone了这个代码仓库,你要先npm install

(顺便说下,把测试脚本放在根目录下的test文件夹中是测试的惯例,如果你想要让别人认为你写测试很专业,那你也应该这么做)

执行后,输出长这个样子。

如果有一个测试没有通过,你会看到刺眼的红色,然后就可以马上修改了。

看一下我们的测试用例:

// test-calculator.js
const {describe, it} = require('mocha')
const {expect} = require('chai')
const calculator = require('../../lib/calculator')

describe('calculator', function () {
  const stream = (characters, calculatorState = calculator.initialState) =>
    !characters
      ? calculatorState
      : stream(characters.slice(1),
               calculator.nextState(calculatorState, characters[0]))

  it('should show initial display correctly', () => {
    expect(calculator.initialState.display).to.equal('0')
  })
  it('should replace 0 in initialState', () => {
    expect(stream('4').display).to.equal('4')
  })
//...
复制代码

我们先引入mocha,还有它的断言库expect(稍后我们会将啥事断言库)。引入一些我们需要的函数describeit

然后引入我们要测试的模块——calculator

然后就是使用it函数定义的测试用例了,如下:

it('should show initial display correctly', () => {
    expect(calculator.initialState.display).to.equal('0')
})
复制代码

it函数接受一个字符串参数,用来描述测试用例,另一个参数是一个函数,就是测试本身了。但是it是不能“裸奔”的,需要被包裹在describe函数定义的测试组中。

在测试逻辑中写啥呢?其实啥都可以。在这里我们就是判断了下初始展示的值是不是等于0. 如果不用expect,我们可以这么写:

if (calculator.initialState.display !== '0')
  throw 'failed'
复制代码

Mocha中如果一个测试不通过,就会抛出一个异常,就是这么简单。但是使用expect让我们可以使用他的一些特性来方便地检查数组、对象的值。

这就是单元测试的主旨了——执行一个或者一组函数(如果你是面向对象测试,则通常实例化一个对象,然后调用它的方法),检查返回的结果是否等于预期结果。

编写可测试的代码

单测里最复杂的不是测试本身,而是分离代码,从而让它们尽可能地变得可测。**可单测的代码就是,对其他模块和IO没有依赖的代码。**这并不简单,因为我们通常习惯于讲业务逻辑和IO,UI耦合起来。但是这个目标仍然是可以达到的,有很多技术。例如,如果你有一段验证表单的代码,把它们分离出来变成一个一个的验证函数,然后对它们进行测试。

测试代码运行在Node环境下?

注意到很重要的一点——单测是运行在Node环境下的。即使计算器应用的代码是跑在浏览器中,我们仍然使用Node去跑我们的测试,包括要上线的代码。

这怎么能行呢?这是因为我们的代码是同构的。这意味着它们能同时运行在浏览器和Node环境中。如果你的代码中没有任何IO操作,这就意味着它并不是只能在浏览器中运行。尤其是,我们的代码使用了require来组织代码,既能被NodeJS识别,又能被Webpack打包(看下package.json,你就会发现使用了webpack)。

"scripts": {
   "build": "webpack && cp public/* dist",
   ...
}
复制代码

在浏览器环境下单测

顺便提一下,我们可以使用karma来实现在浏览器中运行Mocha。但是我谨认为如果能在Node中运行就在Node中运行(现在你其实很容易写出在两端都能运行的代码),因为无论是运行还是debug都更方便。不编译代码的话,运行起来会更快。

但是不在浏览器中跑测试,我们就不能确认我们的代码能在浏览器中运行。两个环境中的一些细微差别可能会导致一些问题。

下周

上面说的问题就是E2E测试要管的事了——在真实的浏览器环境中测试我们的代码。下周我们将如何写E2E测试。

()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值