测试你的前端代码 - part3(端到端测试)

本文做者:Gil Tayar
编译:胡子大哈 javascript

翻译原文:http://huziketang.com/blog/posts/detail?postId=58d50da37413fc2e8240855c
英文链接:Testing Your Frontend Code: Part III (E2E Testing)css

转载请注明出处,保留原文连接以及做者信息html

上一篇文章《测试你的前端代码 - part2(单元测试)》中,我介绍了关于单元测试的基本知识,从本文介绍端到端测试(E2E 测试)。前端

端到端测试

第二部分中,咱们使用 Mocha 测试了应用中最核心的逻辑,calculator 模块。本文中咱们将使用端到端测试整个应用,其实是模拟了用户全部可能的操做进行测试。java

在咱们的例子中,计算器展现出来的前端即为整个应用,由于没有后端。因此端到端测试就是说直接在浏览器中运行应用,经过键盘作一系列计算操做,且保证所展现的输出结果都是正确的。node

是否须要像单元测试那样,测试各类组合呢?并非,咱们已经在单元测试中测试过了,端到端测试不是检查某个单元是否 ok,而是把它们放到一块儿,检查仍是否可以正确运行。react

须要多少端到端测试

首先给出结论:端到端测试不须要太多。webpack

第一个缘由,若是已经经过了单元测试和集成测试,那么可能已经把全部的模块都测试过了。那么端到端测试的做用就是把全部的单元测试绑到一块儿进行测试,因此不须要不少端到端测试。git

第二个缘由,这类测试通常都很慢。若是像单元测试那样有几百个端到端测试,那运行测试将会很是慢,这就违背了一个很重要的测试原则——测试迅速反馈结果。github

第三个缘由,端到端测试的结果有时候会出现 flaky 的状况。Flaky 测试是指一般状况下能够测试经过,可是偶尔会出现测试失败的状况,也就是不稳定测试。单元测试几乎不会出现不稳定的状况,由于单元测试一般是简单输入,简单输出。一旦测试涉及到了 I/O,那么不稳定测试可能就出现了。那能够减小不稳定测试吗?答案是确定的,能够把不稳定测试出现的频率减小到能够接受的程度。那可以完全消除不稳定测试吗?也许能够,可是我到如今还没见到过[笑着哭]。

因此为了减小咱们测试中的不稳定因素,尽可能减小端到端测试。10 个之内的端到端测试,每一个都测试应用的主要工做流。

写端到端测试代码

好了,废话很少说,开始介绍写端到端代码。首先须要准备好两件事情:1. 一个浏览器;2. 运行前端代码的服务器。

由于要使用 Mocha 进行端到端测试,就和以前单元测试同样,须要先对浏览器和 web 服务器进行一些配置。使用 Mocha 的 before 钩子设置初始化状态,使用 after 钩子清理测试后状态。before 和 after 钩子分别在测试的开始和结束时运行。

下面一块儿来看下 web 服务器的设置。

设置 Web 服务器

配置一个 Node Web 服务器,首先想到的就是 express 了,话很少说,直接上代码

let server
      
      before((done) => {
        const app = express()
        app.use('/', express.static(path.resolve(__dirname, '../../dist')))
        server = app.listen(8080, done)
      })
      after(() => {
        server.close()
      })

代码中,before 钩子中建立一个 express 应用,指向 dist 文件夹,而且监听 8080 端口,结束的时候在 after 钩子中关闭服务器。

dist 文件夹是什么?是咱们打包 JS 文件的地方(使用 Webpack打包),HTML 文件,CSS 文件也都在这里。能够看一下 package.json 的代码:

{
      "name": "frontend-testing",
      "scripts": {
        "build": "webpack && cp public/* dist",
        "test": "mocha 'test/**/test-*.js' && eslint test lib",
    ...
      },

对于端到端测试,要记得在执行 npm test 以前,先执行 npm run build。其实这样很不方便,想一下以前的单元测试,不须要作这么复杂的操做,就是由于它能够直接在 node 环境下运行,既不用转译,也不用打包。

出于完整性考虑,看一下 webpack.config.js 文件,它是用来告诉 webpack 怎样处理打包:

module.exports = {
      entry: './lib/app.js',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
      },
      ...
    }

上面的代码指的是,Webpack 会读取 app.js 文件,而后将 dist 文件夹中全部用到的文件都打包到 bundle.js 中。dist 文件夹会同时应用在生产环境和端到端测试环境。这里要注意一个很重要的事情,端到端测试的运行环境要尽可能和生产环境保持一致。

设置浏览器

如今咱们已经设置完了后端,应用已经有了服务器提供服务了,如今要在浏览器中运行咱们的计算器应用。用什么包来驱动自动执行程序呢,我常用 selenium-webdriver,这是一个很流行的包。

首先看一下如何使用驱动:

const {prepareDriver, cleanupDriver} = require('../utils/browser-automation')
    
    //...
    describe('calculator app', function () {
      let driver
      ...
      before(async () => {
        driver = await prepareDriver()
      })
      after(() => cleanupDriver(driver))
    
      it('should work', async function () {
        await driver.get('http://localhost:8080')
        //...
      }) 
    })

before 中,准备好驱动,在 after 中把它清理掉。准备好驱动后,会自动运行浏览器(Chrome,稍后会看到),清理掉之后会关闭浏览器。这里注意,准备驱动的过程是异步的,返回一个 promise,因此咱们使用 async/await 功能来使代码看起来更美观(Node7.7,第一个本地支持 async/await 的版本)。

最后在测试函数中,传递网址:http:/localhost:8080,仍是使用 await,让 driver.get 成为异步函数。

你是否有好奇 prepareDrivercleanupDriver 函数长什么样呢?一块儿来看下:

const webdriver = require('selenium-webdriver')
    const chromeDriver = require('chromedriver')
    const path = require('path')
    
    const chromeDriverPathAddition = `:${path.dirname(chromeDriver.path)}`
    
    exports.prepareDriver = async () => {
      process.on('beforeExit', () => this.browser && this.browser.quit())
      process.env.PATH += chromeDriverPathAddition
    
      return await new webdriver.Builder()
        .disableEnvironmentOverrides()
        .forBrowser('chrome')
        .setLoggingPrefs({browser: 'ALL', driver: 'ALL'})
        .build()
    }
    
    exports.cleanupDriver = async (driver) => {
      if (driver) {
        driver.quit()
      }
      process.env.PATH = process.env.PATH.replace(chromeDriverPathAddition, '')
    }

能够看到,上面这段代码很笨重,并且只能在 Unix 系统上运行。理论上,你能够不用看懂,直接复制/粘贴到你的测试代码中就能够了,这里我仍是深刻讲一下。

前两行引入了 webdriver 和咱们使用的浏览器驱动 chromedriver。Selenium Webdriver 的工做原理是经过 API(第一行中引入的 selenium-webdriver)调用浏览器,这依赖于被调浏览器的驱动。本例中被调浏览器驱动是 chromedriver,在第二行引入。

chrome driver 不须要在机器上装了 Chrome,实际上在你运行 npm install 的时候,已经装了它自带的可执行 Chrome 程序。接下来 chromedriver 的目录名须要添加进环境变量中,见代码中的第 9 行,在清理的时候再把它删掉,见代码中第 22 行。

设置了浏览器驱动之后,咱们来设置 web driver,见代码的 11 - 15 行。由于 build 函数是异步的,因此它也使用 await。到如今为止,驱动部分就已经设置完毕了。

测试吧!

设置完驱动之后,该看一下测试的代码了。完整的测试代码在这里,下面列出部分代码:

// ...
    const retry = require('promise-retry')
    // ...
    
      it('should work', async function () {
        await driver.get('http://localhost:8080')
    
        await retry(async () => {
          const title = await driver.getTitle()
    
          expect(title).to.equal('Calculator')
        })
        //...

这里的代码调用计算器应用,检查应用标题是否是 “Calculator”。代码中第 6 行,给浏览器赋地址:http://localhost:8080,记得要使用 await。再看第 9 行,调用浏览器而且返回浏览器的标题,在第 10 行中与预期的标题进行比较。

这里还有一个问题,这里引入了 promise-retry 模块进行重试,为何须要重试?缘由是这样的,当咱们告诉浏览器执行某命令,好比定位到一个 URL,浏览器会去执行,可是是异步执行。浏览器执行的很是快,这时候对于开发人员来说,确切地知道浏览器“正在执行”,要比仅仅知道一个结果更重要。正是由于浏览器执行的很是快,因此若是不重试的话,很容易被 await 所愚弄。在后面的测试中 promise-retry 也会常用,这就是为何在端到端测试中须要重试的缘由。

测试 Element

来看测试的下一阶段,测试元素:

const {By} = require('selenium-webdriver')
      it('should work', async function () {
        await driver.get('http://localhost:8080')
        //...
        
        await retry(async () => {
          const displayElement = await driver.findElement(By.css('.display'))
          const displayText = await displayElement.getText()
    
          expect(displayText).to.equal('0')
        })
        
        //...

下一个要测试的是初始化状态下所显示的是否是 “0”,那么首先就须要找到控制显示的 element,在咱们的例子中是 display。见第 7 行代码,webdriver 的 findElement 方法返回咱们所要找的元素。能够经过 By.id 或者 By.css 再或者其余找元素的方法。这里我使用 By.css,它很经常使用,另外提一句 By.javascript 也很经常使用。

(不知道你是否注意到,By 是由最上面的 selenium-webdriver 所引入的)

当咱们获取到了 element 之后,就可使用 getText()(还可使用其余操做 element 的函数),来获取元素文本,而且检查它是否和预期同样,见第 10 行。对了,不要忘记:

测试 UI

如今该来从 UI 层面测试应用了,点击数字和操做符,测试计算器是否是按照预期的运行:

const digit4Element = await driver.findElement(By.css('.digit-4'))
        const digit2Element = await driver.findElement(By.css('.digit-2'))
        const operatorMultiply = await driver.findElement(By.css('.operator-multiply'))
        const operatorEquals = await driver.findElement(By.css('.operator-equals'))
    
        await digit4Element.click()
        await digit2Element.click()
        await operatorMultiply.click()
        await digit2Element.click()
        await operatorEquals.click()
    
        await retry(async () => {
          const displayElement = await driver.findElement(By.css('.display'))
          const displayText = await displayElement.getText()
    
          expect(displayText).to.equal('84')
        })

代码 2 - 4 行,定义数字和操做;6 - 10 行模拟点击。实际上想实现的是 “42 * 2 = ”。最终得到正确的结果——“84”。

运行测试

已经介绍完了端到端测试和单元测试,如今用 npm test 来运行全部测试:

一次性所有经过!(这是固然的了,否则怎么写文章。)

想说点关于使用 await 的一些话

你在可能网络上其余地方看到一些例子,它们并无使用 async/await,或者是使用了 promise。实际上这样的代码是同步的。那么为何也能 work 的很好呢?坦白地说,我也不知道,看起来像是在 webdriver 中有些 trick 的处理。正如 selenium 文档中说道,在 Node 支持 async/await 以前,这是一个临时的解决方案。

Selenium 文档是 Java 语言。它还不完整,可是包含的信息也足够了,你作几回测试就能掌握这个技能。

总结

本文中主要介绍了什么:

  • 介绍了端到端测试中设置浏览器的代码;

  • 介绍了如何使用 webdriver API 来调用浏览器,以及如何获取 DOM 中的 element;

  • 介绍了使用 async/await,由于全部 webdriver API 都是异步的;

  • 介绍了为何端到端测试中要使用 retry。

下文简介

介绍完了端到端测试,下文该介绍“测试光谱”的中间部分了,即集成测试。连接直达:《测试你的前端代码 - part4(集成测试)》


我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点

相关文章
相关标签/搜索