您的位置:时时app平台注册网站 > web前端 > 测试你的前端代码 – part4(集成测试)彩世界

测试你的前端代码 – part4(集成测试)彩世界

2019-10-12 18:43

在浏览器中运行单元测试

我们还可以使用另一个测试框架,Karma。使用它可以在浏览器中运行 Mocha 代码,但是这里表达一下我的浅见:单元测试能在 Node 下运行就在 Node 下运行,因为很容易执行和 debug(当然现在在浏览器中执行也很方便)。并且如果代码不需要转译的话,执行的也非常快。

但是我们的代码没有在浏览器中测试确实是个问题,因为我们并不真正地知道代码在浏览器中运行会是什么样子。浏览器中的 JS 执行环境和 NodeJS 环境可能会有微妙的差别。

事件处理

剩下的测试代码怎么写呢,看下面代码:

JavaScript

ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0') const digit4Element = document.querySelector('.digit-4') const digit2Element = document.querySelector('.digit-2') const operatorMultiply = document.querySelector('.operator-multiply') const operatorEquals = document.querySelector('.operator-equals') digit4Element.click() digit2Element.click() operatorMultiply.click() digit2Element.click() operatorEquals.click() expect(displayElement.textContent).to.equal('84')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReactDom.render(e(CalculatorApp), document.getElementById('container'))
const displayElement = document.querySelector('.display')
expect(displayElement.textContent).to.equal('0')
const digit4Element = document.querySelector('.digit-4')
const digit2Element = document.querySelector('.digit-2')
const operatorMultiply = document.querySelector('.operator-multiply')
const operatorEquals = document.querySelector('.operator-equals')
digit4Element.click()
digit2Element.click()
operatorMultiply.click()
digit2Element.click()
operatorEquals.click()
expect(displayElement.textContent).to.equal('84')

测试中主要实现的是用户点击 “42 * 2 = ”,结果应该是输出 “84”。这里获取 element 使用的是广为人知的 querySelector 函数,然后调用 click 点击。还可以创建事件,然后手动调度,见下面代码:

JavaScript

var ev = new Event("keyup", ...); document.dispatchEvent(ev);

1
2
var ev = new Event("keyup", ...);
document.dispatchEvent(ev);

这里有内置的 click 函数,所以我们直接使用就好了。就是这么简单!

机智的你可能已经发现了,这个测试和前面的端到端测试其实是一样的。但是注意这个测试要快 10 倍以上,并且实际上它是同步的,代码也更容易写,可读性也更好。

但是如果都一样的话,那需要继承测试干嘛?因为这是个示例项目嘛,并不是实际项目。这个项目里面只有两个组件,所以端到端测试和继承测试是一样的。如果是在实际项目中,端到端测试可能包含了上百个单元,而继承测试只包含少量单元,比如包含 10 个单元。所以实际项目中只有几个端到端测试,而可能包含了上百个继承测试。

“calculator”模块

处理逻辑问题的代码是一个单独的模块——calculator。这个模块对于单元测试是很完美的例子。因为它没有对 I/O 和 UI 的依赖。你也应该尽量使你的应用逻辑上保持独立——模块不依赖于 I/O 和 UI。

对于 Web 应用来讲,I/O 是什么?没有文件和数据库的操作?其实不仅仅是这样,还有 Ajax 调用,本地存储,DOM 操作等,对我而言,任何和浏览器 API 有关的都是 I/O 操作。

我是怎么把计算逻辑从 React 组件中分离出来的呢?其实很简单,其内在逻辑是计算,我把他封装到一个模块中就可以了。

这个模块的实现也很容易——它接收一个计算器 state(一个对象)和一个字符(数字或者操作符),返回一个新的计算器 state。如果你用过 Redux,它很像 Redux 的 reducer 模式(如果你没用过 Redux 也没关系)。但是如果一直由上一个 state 获取下一个 state,怎么能回到初始状态呢?这里还有一个模块叫做 initialState,通过它可以初始化计算器。计算器的 state 并不是完全黑盒的,它包含了一个字段叫做 display,可以把你想要展示的 state 显示在计算器应用上。

如果你没有耐心看源代码的话,我们一起来看下这里面最重要的部分,应用中算法的细节其实不重要。

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 } } //....

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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
      }
    }
 
    //....

再次强调这里的实现细节并不重要,重要的是模块的设计,它暴露出来的函数非常简单——给一个 state,得到下一个 state。

这就是我们在 test-calculator 中所做的事情。那么接下来怎么进行测试呢?使用测试框架,目前比较流行的框架是Mocha,我们就用它。不过像 Jest,Jasmine,Tape等框架也都行,随意使用你喜欢的测试框架。

测试你的前端代码 – part4(集成测试)

2017/06/05 · 基础技术 · 测试

原文出处: Gil Tayar   译文出处:胡子大哈   

上一篇文章《测试你的前端代码 – part3(端到端测试)》中,我介绍了关于端到端测试的基本知识,从本文介绍集成测试(Integration Testing)。

测试你的前端代码 – part2(单元测试)

2017/04/21 · 基础技术 · 测试

原文出处: Gil Tayar   译文出处:胡子大哈   

彩世界网址 1

上一篇文章《测试你的前端代码 – part1(介绍)》中,我介绍了关于前端测试的基本知识,从本文开始将具体介绍测试技术。

总结

本文中主要介绍了什么:

  • 介绍了使用 jsdom 方便地创建全局变量 documentwindow
  • 介绍了如何使用 jsdom 测试应用;
  • 介绍了,测试就是这么简单^_^。

    1 赞 收藏 评论

彩世界网址 2

测试代码是运行在 NodeJS 下的!?

注意一个重要的事情——单元测试是在 NodeJS 下运行的!而计算器应用是运行在浏览器端的,上面的生产代码都是在 NodeJS 下进行测试的,这也可以吗?

当然可以。因为我们的代码是同构的,它可以运行在浏览器端和 NodeJS 上。如果你的代码没有使用任何 I/O,就是说没有对浏览器做任何的特化处理,那么它就没有理由不能运行在 NodeJS 上。另外,如果你使用了 require,它既可以被本地的 NodeJS 识别,也可以被像 Webpack 一样的打包器识别。你看代码中的 package.json,就可以看到我们就是使用了 Webpack,用 require 进行代码打包:

"scripts": { "build": "webpack && cp public/* dist", ... }

1
2
3
4
    "scripts": {
       "build": "webpack && cp public/* dist",
       ...
    }

代码中使用 require 来引入 React 或者其他模块,这不论是在 NodeJS 中还是浏览器中都是通用的。

关于术语

和许多 TDD 爱好者聊过以后,我了解了他们对“集成测试”这个词有一些不同的理解。他们认为集成测试是测试代码边界,即代码对外的接口部分。

比如他们代码中有 Ajax,localStorage 或者 IndexedDB 操作,那其代码就不能做单元测试,这时他们会把这些代码打包成接口,然后在做单元测试的时候 mock 这些接口。当真正测试这些接口的时候才称作“集成测试”。从这个角度来说,“集成测试”就是在纯的单元测试以外,测试与外部“真实世界”相关的代码。

而我和其他一些人则倾向于认为“集成测试”是将两个或多个单元测试综合起来进行测试的一种方法。通过接口把与外部相关的代码打包到一起,再 mock,只是其中的一种实现方式。

我的观点里,决定是否使用真实场景的 Ajax 或者其他 I/O 操作进行集成测试(即不使用 mock),取决于是否能够保证测试速度足够快,并且能够稳定测试(不发生 flaky 的情况)。如果可以确定这样的话,那尽管用真实场景进行集成测试就好了。不过如果很慢或者发生不稳定测试的情况,那还是用 mock 会好一些。

在我们的例子中,计算器应用唯一的真实 I/O 就是操作 DOM 了,没有 Ajax 调用,所以不存在上面的问题。

用 Mocha 进行单元测试

所有的测试框架都类似,写测试代码调用被测函数,通过测试框架运行他们,其中运行它们的代码通常叫做“runner”。

Mocha runner 叫做 “mocha”,如果你看测试脚本的 package.json,可以看到:

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

1
2
3
4
5
    "scripts": {
    ...
        "test": "mocha 'test/**/test-*.js' && eslint test lib",
    ...
    },

它会运行 test 文件夹中所有以 test- 开头的文件,你可以复制我的 repo,npm install 后,运行 npm test 自己试试。

(顺便提一句,把所有测试都放在测试目录,并且测试目录放在 package 的根目录是一个公认的 npm package 约定,如果你不想让人觉得你不专业的话,最好还是遵守这一约定。)

运行它,会得到如下输出:

彩世界网址 3

这里有 14 个测试通过的提示信息,如果没通过,就会有红色提示出现。

我们看下面代码:

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') }) //...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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,这里只引入我们需要的函数:describe,it 和 expect。接下来引入我们要测试的模块 calculator。

准备开始测试,用 it 函数来表达:

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

1
2
3
    it('should show initial display correctly', () => {
        expect(calculator.initialState.display).to.equal('0')
    })

it 函数接收一个字符串(用来表示测试结果)和一个函数(待测函数)。it 测试不能单独运行,它们必须组成一个测试组。所以如代码中所示,用 describe 函数定义测试组,里面包含了若干个 it 函数。

测试函数中写什么呢?可以写任何想写的东西,在这个例子中我们测试了初始状态所显示的是不是 0。如果我们自己来实现怎么实现呢,比如可以像如下代码:

if (calculator.initialState.display !== '0') throw 'failed'

1
2
    if (calculator.initialState.display !== '0')
      throw 'failed'

对于这个问题,上面代码也是可以测出来的。但是 expect 包含了很多特性可以使测试变得更简单,比如可以测试数组或者对象是否和一个给定的值相等。这就是单元测试的要点,即运行一个函数,或一组函数,检查其 运行结果 是否和 预期结果 一致。

歉意

这一部分是这个测试系列文章中唯一使用指定框架的部分,这部分使用的框架是 React。选择 React 并不是因为它是最好的框架,我坚定地认为没有所谓最好的框架,我甚至认为对于指定的场景也没有最好的框架。我相信的是对于个人来讲,只有最合适,用着最顺手的框架。

而我使用着最顺手的框架就是 React,所以接下来的代码都是 React 代码。但是这里依然说明一下,前端集成测试的 jsdom 解决方案可以适用于所有的主流框架。

ok,现在回到正题。

单元测试

上一节有讨论过,单元测试就是以代码单元为单位进行测试,代码单元可以是一个函数,一个模块,或者一个类。很多人认为大多数测试都应该叫单元测试,其实我的观点还是那句话,无所谓怎么叫,名字叫什么都行。只要你做了足够多的测试,能够保证你部署到线上的生产代码没有问题就可以了。

单元测试是最容易理解、也最容易实现的测试方式。给单元测试一个输入,让它自动执行,将输出结果和预期结果做对比看其是否正确(输入可以是一个函数参数,输出就是函数的返回值)。

在写单元测试的时候,尽量将你的单元测试独立出来,不要几个单元互相引用。养成这样良好的测试习惯。

集成测试

我们已经看过了“测试光谱”中的两种测试:单元测试和端到端测试。实际工作中的测试经常是介于这两种测试之间的,包括我在内的大多数人通常把这种测试叫做集成测试。

测试 Calculator 应用

第一节中提到过,为了这系列博文,我写了一个计算器应用,后面都会拿它进行测试。理论就讲到这里,一起来看一下 Calculator 应用吧,源代码在这里。主要有两个组件: keypad 和display,它们自身都是 React 单元,也都没有引用其他单元,后面会介绍如何对它们进行测试。

(如果你已经看了代码可能已经发现了我没有使用 JSX。因为我不想进行转译。现在 Node 和所有流行的浏览器都已经完全支持 ES6 了,那么作为一个例子来讲,让它直接运行会更好一些。虽然它不能运行在 IE 上,不过也没关系,如果是一个真实的线上项目,我会进行转译的。)

还有一个问题是按键和展示的逻辑问题,必须要有代码来控制当点击按键的时候发生什么。这里的按键包括数字键(如“1”,“5”)和操作键(如“ ”,“=”)。按通常的做法,我把组件设计成了展示型组件(键盘)和容器型组件。容器型组件在我的 App 中是唯一包含 state 的组件,它要考虑当发生按键行为的时候 App 内在逻辑的问题。

使用 Jsdom

JavaScript

const React = require('react') const e = React.createElement const ReactDom = require('react-dom') const CalculatorApp = require('../../lib/calculator-app') ... describe('calculator app component', function () { ... it('should work', function () { ReactDom.render(e(CalculatorApp), document.getElementById('container')) const displayElement = document.querySelector('.display') expect(displayElement.textContent).to.equal('0')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const React = require('react')
const e = React.createElement
const ReactDom = require('react-dom')
const CalculatorApp = require('../../lib/calculator-app')
    ...
describe('calculator app component', function () {
        ...
    it('should work', function () {
        ReactDom.render(e(CalculatorApp), document.getElementById('container'))
        const displayElement = document.querySelector('.display')
        expect(displayElement.textContent).to.equal('0')

注意看第 10 – 14 行,首先 render 了 CalculatorApp 组件,这个操作同时也 render 了 DisplayKeypad。第 12 和 14 行测试了 DOM 中计算器的显示是否是 0(初始化状态下)。

上面的代码是可以运行在 Node 下的,注意到里面用的是 document。我第一次使用它的时候特别惊讶。全局变量 document 是一个浏览器变量,竟然可以使用在 NodeJS 中。在这简单的几行代码背后有着大量的代码支撑着,这些 jsdom 代码几乎是完美地实现了浏览器的功能。所以这里我要感谢 Domenic Denicola, Elijah Insua 和为这个工具包做过贡献的人们。

彩世界网址 4

第 10 行中也使用了 document(调用 ReactDom 来渲染组件),在 ReactDom 经常会使用它。那么在哪里创建的这些全局变量呢?在测试中创建的,见下面代码:

JavaScript

before(function () { global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`) global.window = document.defaultView }) after(function () { delete global.window delete global.document })

1
2
3
4
5
6
7
8
9
before(function () {
        global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`)
        global.window = document.defaultView
      })
 
    after(function () {
        delete global.window
        delete global.document
      })

代码中创建了一个简单的 document,把我们的组件挂在一个简易 div 上。同时还创建了一个 window,其实我们并不需要它,但是 React 需要。最后在 after 中清理全局变量。

documentwindow 一定要设置成全局的吗?滥用全局变量不论理论和实践的角度都不是个好习惯。如果它们是全局的,那这个集成测试就不能和其他的集成测试并行运行(这里对 ava 的用户表示抱歉),因为它们会互相覆写全局变量,导致结果错误。

然而,它们必须要设置成全局的,React 和 ReactDOM 要求 documentwindow 是全局的,不接受把他们以参数的形式传递。或许等 React fiber 出来就可以了?也许吧,不过现在我们还必须要把 documentwindow 设置成全局的。

总结

本文中主要介绍了什么:

  • 介绍了如何使用 Mocha (和 Chai)创建单元测试;
  • 介绍了单元测试就是以代码单元为单位进行测试,这个代码单元是独立于其他模块的。
  • 介绍了设计模块时应该独立于其他模块。如果一定要有依赖,那么可以 mock 一个其他模块对本模块进行单元测试,或者进行集成测试。
  • 介绍了我们测试的代码单元应该是同构的,这样就可以在 NodeJS 环境下进行测试了。
  • 介绍了如何写同构代码——没有 I/O操作、使用 require 引入模块、使用 Webpack 来打包模块以使其符合浏览器运行环境。

    1 赞 收藏 评论

彩世界网址 5

mock DOM

这就引出了一个问题:在集成测试中是否需要 mock DOM?重新思考一下上面我说的标准,使用真实 DOM 是否会使测试变慢呢,答案是会的。使用真实 DOM 意味着要用浏览器,用浏览器意味着测试速度变慢,测试变的不稳定。

那么是不是要么只能尽量把操作 DOM 的代码分离出来,要么只能使用端到端测试了呢?其实这两种方法都不好。还有另一种解决方案:jsdom。一个非常棒的包,用它自己的话说:这是在 NodeJS 中实现的 DOM。

它确实比较好用,可以运行在 Node 环境下。使用 JSDom,你可以不把 DOM 当做 I/O 操作。这一点非常重要,因为要把 DOM 操作从前端代码中分离出来非常困难(实际工作中几乎不可能完全分离)。我猜 JSDom 的诞生就是因为这个原因:使得在 Node 中也可以运行前端测试。

我们来看一下它的工作原理,和往常一样,需要有初始化代码和测试代码。这次我们先看测试代码。不过正式看代码之前请先接受我的歉意。

编写单元可测的代码

上面的很简单对吧!其实对于单元测试来讲,难的并不是单元测试本身,而是分离代码的艺术,把代码尽量分离成单元可测的模块。单元可测的代码一般都是不依赖于其他模块、不依赖于 I/O 的代码。这是比较困难的,大多数人都倾向于把逻辑代码、I/O 代码和 UI 代码写到一起。困难是困难,但不是说做不到,有很多技巧可以使用,比如你的代码中有一些验证字段,那么你就可以把验证代码组织到一起形成函数,再对这个验证函数进行测试。

本文由时时app平台注册网站发布于web前端,转载请注明出处:测试你的前端代码 &#8211; part4(集成测试)彩世界

关键词:

  • 上一篇:CSS再学
  • 下一篇:没有了