来自 软件资讯 2019-09-19 08:22 的文章
当前位置: 威尼斯国际官方网站 > 软件资讯 > 正文

端到端测量试验,测量试验你的前端代码

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

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

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

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

测试你的前端代码:可视化测试

2017/10/25 · CSS · 1 评论 · 测试

原文出处: Gil Tayar   译文出处:oschina   

测试 App,你从哪里开始?在最后这个部分,第五部分,Gil Tayar 总结了他为前端测试新人写的系列文章。最后这篇文章中,Tayar 讲述了可视化测试,以及为什么它是测试前端代码的最后一步。

不久前,我一个刚刚进入精彩前端世界的朋友打电话问我该怎么测试他的应用程序。我告诉她有太多需要学习的东西,在电话里根本说不清楚。我答应发送一些对她前端之路有所帮助的链接。

所以我在电脑前坐下,通过 Google 搜索相关的主题。我找到很多链接,也发送给她了,但我对这些链接讨论的深度并不满意。我找不到一个全面的指南 —— 以新入行的前端的角度 —— 指导如何测试前端应用。我没找到某个指南既讲理论又讲实践,同时还是面向前端应用的讨论。

因此,我决定写一个。这已经是这一系列的第五部分了。你可以在下面看到其它部分:

  • 介绍
  • 单元测试
  • 端到端(E2E)测试
  • 集成测试
  • 可视化测试

另外,为了写这篇文章,我写了一个小应用 —— Calculator(计算器) —— 我要用它演示测试的不同类型。你可以在这里看到它的源代码。

端到端测试

在第二部分中,我们使用 Mocha 测试了应用中最核心的逻辑,calculator模块。本文中我们将使用端到端测试整个应用,实际上是模拟了用户所有可能的操作进行测试。

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

是否需要像单元测试那样,测试各种组合呢?并不是,我们已经在单元测试中测试过了,端到端测试不是检查某个单元是否 ok,而是把它们放到一起,检查还是否能够正确运行。

可视化测试

软件测试一直是我的一大爱好。最近,我觉得没有测试就写不出代码。对我来说,有一种原始的想法,运行的目的就是为了验证代码是否正确。你的意思是告诉我,在以前,每次开发者修改他们的代码,都需要有人手工去验证之前正常的事情仍然正常?是这样吗?

因此,我写测试。因为我喜欢演讲和写博客,我会演讲或写关于软件测试的内容。如果有机会进入一个对加强软件测试有着卓越远见的公司,写代码来帮助其它人写测试,并推广他们的产品,我会毫不犹豫的加入。

正是如此,我最近加入了 Applitools (如果你想知道职位,是布道师和高级架构师)。因为他们的产品,Applitools Eyes,与我写的这个系列有着直接联系,我决定在这个系列中多写一个部分 —— 一个关于“可视化测试”的部分。

还记得我的疑惑吗?开发者实际总是会在每次修改他们的代码之后运行他们的应用。嗯,到目前为止,软件产品需要手工测试 —— 这是在应用的可视化方面。还没有办法检查应用看起来仍然是好的 —— 字体是正确的,对齐没有问题,颜色也还在,等等。

理论上你是可以写代码来进行相关的检查。我们在第三部分了解到如何使用 Selenium Webdriver 测试 Web 应用的 UI。我们可以使用 Selenium 的 getScreenShot API 来获得页面的截图,将其保存为基准,之后每个测试都会将页面截图与这个基准进行比较:

图片 1

啊哈!要是这么简单就好了。我尝试过这个方案,结果遇到不少问题,最后不得不放弃这个方案。而且可笑的是我每次修改了代码都要运行应用。主要的问题在某些技术:浏览器在呈现内容的时候存在一些细微的差异 —— 造成这些差异的因素可能来源于屏幕或者 GPU,对内容进行抗锯齿渲染的方式略有不同。没有两张截图会拥有完全一样的像素。这些差异人眼觉察不到,也就是说,按像素进行比较毫无意义。你需要使用图像分析技术来处理这个问题。

而且,还有其它问题,仅从我基于 Applitools 的工作就能总结出如下问题:

  • 你不能对整个页面截图 —— 你只能对可以看到的部分截图。
  • 如果页面中存在动画,那就不能拿它和基础图像进行比较。
  • 动态数据,比如广告,会让事情变得复杂,难以找出与基准相比的实际差异。
  • 页面什么时候才会“完全加载”?什么时候才能对其截图?现在在 DOM 加载完成时截图是不够的。要找出什么时候才可以截图是件非常困难的事情。

需要多少端到端测试

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

第一个原因,如果已经通过了单元测试和集成测试,那么可能已经把所有的模块都测试过了。那么端到端测试的作用就是把所有的单元测试绑到一起进行测试,所以不需要很多端到端测试。

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

第三个原因,端到端测试的结果有时候会出现 flaky的情况。Flaky 测试是指通常情况下可以测试通过,但是偶尔会出现测试失败的情况,也就是不稳定测试。单元测试几乎不会出现不稳定的情况,因为单元测试通常是简单输入,简单输出。一旦测试涉及到了 I/O,那么不稳定测试可能就出现了。那可以减少不稳定测试吗?答案是肯定的,可以把不稳定测试出现的频率减少到可以接受的程度。那能够彻底消除不稳定测试吗?也许可以,但是我到现在还没见到过[笑着哭]。

所以为了减少我们测试中的不稳定因素,尽量减少端到端测试。10 个以内的端到端测试,每个都测试应用的主要工作流。

我们做得到

不过我们似乎可以编写自动的可视化测试。存在着无数我并不知道的工具可以更好的截图并将之与标准图像比较。其中一些如下:

  • Wraith
  • WebdriverCSS
  • 当然还有 Applitools Eyes
  • (还是其它的,但本文已经有点长了…)

这些工具可以解决全部或部分上面提到的问题。在系列的这个部分,我想向你展示如何使用 Applitools Eyes 来编写可视化测试。

写端到端测试代码

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

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

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

写一个可视化测试

既然可视化测试是测试的最终产品,它们应该用于端到端浏览器的前端测试中。所以这是我的可视化测试。这个代码非常有意思,它比常规的端到端测试更小。它由三个部分组成 —— 设置浏览器,测试 Applitools Eyes 和测试本身。

我们再看一下 Selenium Driver 浏览器设置,它与第三部分的端到端测试相同:

let driver before(async () => { driver = new webdriver.Builder().forBrowser('chrome').build() }) after(async () => await driver.quit())

1
2
3
4
5
6
let driver
before(async () => {
  driver = new webdriver.Builder().forBrowser('chrome').build()
})
after(async () => await driver.quit())

这会打开一个浏览器并等待驱动命令。不过在开始测试之前,我们需要安装(以及拆卸)Applitools Eyes:

const {Eyes} = require('eyes.selenium') let eyes before(async () => { eyes = new Eyes() eyes.setApiKey(process.env.APPLITOOLS_APIKEY) await eyes.open(driver, 'Calculator App', 'Tests', {width: 800, height: 600}) }) after(async () => await eyes.close())

1
2
3
4
5
6
7
8
9
10
11
const {Eyes} = require('eyes.selenium')
let eyes
before(async () => {
  eyes = new Eyes()
  eyes.setApiKey(process.env.APPLITOOLS_APIKEY)
  await eyes.open(driver, 'Calculator App', 'Tests', {width: 800, height: 600})
})
after(async () => await eyes.close())

我们创建了一些新的 Eyes(第5行),并打开它们(第8行)—— 可爱的术语,不是吗?不要忘了从 Applitools 获取一个 API 的 Key,这是我们会在下一小节讨论的东西,然后把它设置给 Eyes(第6行)。

现在我们已经设置好浏览器和 Eyes,我们可以写测试了,这和我们的端到端测试非常像:

it('should look good', async function () { await driver.get('') await eyes.checkWindow('Initial Page') 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 eyes.checkWindow('After calculating 42 * 2 =') })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it('should look good', async function () {
   await driver.get('http://localhost:8080')
   await eyes.checkWindow('Initial Page')
   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 eyes.checkWindow('After calculating 42 * 2 =')
})

与这个系列的前一篇文章中的端到端测试相比,你可以看到它很像,但更短。代码中主要的区别是对特定元素的验证被一行简单的代码代替了:

await eyes.checkWindow(‘’)

1
await eyes.checkWindow(‘’)

在端到端测试中,我们是这样做的:

await retry(async () => { const displayElement = await driver.findElement(By.css('.display')) const displayText = await displayElement.getText() expect(displayText).to.equal('0') })

1
2
3
4
5
6
await retry(async () => {
  const displayElement = await driver.findElement(By.css('.display'))
  const displayText = await displayElement.getText()
  expect(displayText).to.equal('0')
})

我们通过重试等待页面“稳定”。但进行可视化测试的时候,你不需要等待页面可见 —— eyes.checkWindow 会帮你干这个事情!

eyes.checkWindow 会截取页面图像并将之与前端测试产生的基准图像进行比较(参阅下面的小节“运行可视化测试”)。如果比较结果是新图像与基准等价,则测试成功,否则测试失败。

设置 Web 服务器

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
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 的代码:

JavaScript

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

1
2
3
4
5
6
7
{
      "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 怎样处理打包:

JavaScript

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

1
2
3
4
5
6
7
8
module.exports = {
    entry: './lib/app.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    ...
}

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

可视化测试是端到端测试更好的工具

进行可视化测试的巨大好处是 —— 系统处理的稳定性。而且 —— 你不是只检查一两个元素 —— 你是在一次断言中检查整个页面。你可能会发现一些压根没想去找的问题!

总的来说,看起来可视化测试是端到端测试中唯一的断言方法。但不幸的是,目前可视化断言较慢,所以你需要好好地把一些检查特定元素的常规断言和检查整个页面的可视化断言组合起来。

记住 —— 没有灵丹妙药:没有某一个测试类型可以做所有事情!混合不同类型的测试可以更好的建立平衡,建议这样的混合需要测试经验。所以现在就开始测试!的确,测试需要时间和责任。但是一旦你开始测试,你就不能回头了。

设置浏览器

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

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

JavaScript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 函数长什么样呢?一起来看下:

JavaScript

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, '') }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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。到现在为止,驱动部分就已经设置完毕了。

运行可视化测试

我们怎么才行运行可视化测试更看到结果?

如果你没有使用环境变量 APPLITOOLS_APIKEY 来提供一个 API Key,npm test 就会跳过可视化测试。所以需要获取一个 API Key 来运行测试,去 Applitools 注册个用户就好。你可以在你的 Applitools 账户界面找到 API Key。把它拷贝下来,用到测试中去(在 Linux/MacOS 中):

APPLITOOLS_APIKEY=<the-api-key> npm test

1
APPLITOOLS_APIKEY=<the-api-key> npm test

如果你使用的是 Windows,那么:

set APPLITOOLS_APIKEY=<the-api-key> && npm test

1
set APPLITOOLS_APIKEY=<the-api-key> && npm test

完成后就可以进行测试了。第一次测试会失败并报告错误 EYES: NEW TEST ENDED。

图片 2

这是因为还没有用于比较的基准。另一方面,如果你看看 Applitools Eyes 界面,会看到:

图片 3

从 Applitools 来看,测试通过了,因为这是一个基准,它假设基准是正确的。你可以通过界面上每个截图的“Like(像)/Unline(不像)”使其“失败”。

第二次运行 npm test,测试会成功:

图片 4

Applitools 界面也会显示为:

图片 5

如果我们故意让测试失败,(比如)通过点击 43 * 3 而不是 42 * 2,测试会失败,Applitools 界面会显示测试并高亮不同之处:

图片 6

修复这个“Bug”需要在 Mocha 和 Applitools 中让测试再次通过。

本文由威尼斯国际官方网站发布于软件资讯,转载请注明出处:端到端测量试验,测量试验你的前端代码

关键词: