十三、编写端到端测试

编写测试是 web 开发的一部分。你的应用越复杂,越大,你就越需要测试它,否则,它会在某个时候崩溃,你会花很多时间修复 bug 和修补东西。在本章中,您将使用 AVA 和 jsdom 为 Nuxt 应用编写端到端测试,并通过 Nightwatch 获得浏览器自动测试的亲身体验。您将学习如何安装这些工具和设置测试环境——让我们开始吧。

我们将在本章中介绍的主题如下:

  • 端到端测试与单元测试
  • 端到端测试工具
  • 使用jsdomn和 AVA 为 Nuxt 应用编写测试
  • 介绍夜表
  • 使用 Nightwatch 为 Nuxt 应用编写测试

端到端测试与单元测试

web 应用通常有两种类型的测试:单元测试和端到端测试。您可能听说过很多关于单元测试的事情,并且在过去做过一些(或加载)。单元测试用于测试应用的小部分和单个部分,而端到端测试则用于测试应用的整体功能。端到端测试包括确保应用功能的集成组件符合预期。换句话说,整个应用都是在真实场景中测试的,与真实用户与应用交互的方式类似。例如,用户登录页面的简化端到端测试可能涉及以下内容:

  1. 加载登录页面。
  2. 为登录表单中的输入提供有效的详细信息。
  3. 点击提交按钮。

  4. 成功登录该页面并查看问候语。

  5. 注销系统。

那么单元测试呢?单元测试运行得很快,使我们能够准确地识别确切的问题和 bug。单元测试的主要缺点是为应用的各个方面编写测试非常耗时。尽管你的应用已经通过了单元测试,但整个应用仍然可能会崩溃。

端到端测试可以一次隐式地测试很多东西,并确保您有一个工作系统。与单元测试相比,端到端测试运行缓慢,并且无法明确指出应用失败的根源。在你的应用中看似无关紧要的部分做一个小小的改变就可以破坏你的整个测试套件。

将应用的单元测试和端到端测试结合起来可能是理想的、令人信服的,因为这样可以对应用进行更彻底的测试,但这可能会非常耗时和昂贵。在本书中,我们将重点介绍端到端测试,因为默认情况下,Nuxt 与您将在下一节中发现的端到端测试工具无缝配置。

端到端测试工具

Nuxt 通过将 AVA 和 jsdom Node.js 模块一起使用,使端到端测试变得非常简单和有趣。但是,在 Nuxt 应用中实现并组合它们进行测试之前,让我们深入研究一下这些 Node.js 模块,看看它们是如何分别工作的,以便您对这些工具有一个坚实的基本了解。让我们在下一节中从 jsdom 开始。

jsdom

简而言之,jsdom是 Node.js 的 W3C 文档对象模型(DOM)的基于 JavaScript 的实现。但是,这意味着什么?我们需要它做什么?想象一下,你需要在服务器端的原始 HTML 中操作 DOM,比如 No.js 应用,比如 Express 和膝关节炎应用,但是服务器端没有 DOM,因此你没有多少可以做的事情。这就是 jsdom 来拯救我们的时候。它将原始 HTML 转换为一个 DOM 片段,该片段与客户端的 DOM 类似,但在 Node.js 内部。然后,您可以使用客户端 JavaScript 库(如 jQuery)像操作符咒一样操作 Node.js 上的 DOM。以下是服务器端应用的基本用法示例:

  1. 在服务器端应用上导入 jsdom:
import jsdom from 'jsdom'
const { JSDOM } = jsdom
  1. 将任何原始 HTML 字符串传递给JSDOM构造函数,您将得到一个 DOM 对象:
const dom = new JSDOM(<!DOCTYPE html><p>Hello World</p>)
console.log(dom.window.document.querySelector('p').textContent)

从前面的代码片段中获得的 DOM 对象具有许多有用的属性,特别是window对象,然后您可以开始像在客户端一样处理传入的 HTML 字符串。现在让我们在 Oracle T3 中使用这个工具,膝关节炎 API API T4,您在前一章中了解到,并且可以在 GITHUB 存储库中的 ORDT1 中找到,打印胡氏 T2 消息。遵循以下步骤:

  1. 通过 npm 安装jsdom和 jQuery:
$ npm i jsdom --save-dev
$ npm i jquery --save-dev
  1. 导入jsdom并传递一个 HTML 字符串,就像我们在前面的基本用法示例中所做的那样:
// src/modules/public/home/_routes/index.js
import Router from 'koa-router'
import jsdom from 'jsdom'

const { JSDOM } = jsdom
const router = new Router()

const html = '<!DOCTYPE html><p>Hello World</p>'
const dom = new JSDOM(html)
const window = dom.window
const text = window.document.querySelector('p').textContent
  1. text输出到端点:
router.get('/', async (ctx, next) => {
  ctx.type = 'json'
  ctx.body = {
    message: text
  }
})

当您在终端上运行npm run dev时,您应该会在localhost:4000/public处看到 JSON 格式的“Hello world”消息(如下代码片段所示):

{"status":200,"data":{"message":"Hello world"}}
  1. 在我们的 API 中创建一个movie模块,使用 Axios 从 IMDb 网站获取一个 HTML 页面,将 HTML 传递给 JSDOM 构造函数,导入 jQuery,然后将其应用于 JSDOM 创建的 DOM 窗口对象,如下所示:
// src/modules/public/movie/_routes/index.js
const url = 'https://www.imdb.com/movies-in-theaters/'
const { data } = await axios.get(url)

const dom = new JSDOM(data)
const $ = (require('jquery'))(dom.window)

Note that Axios must be installed in your project directory via npm, which you do with npm i axios.

  1. 将 jQuery 对象应用于list_item类的所有电影,并按如下方式提取数据(每部电影的名称和放映时间):
var items = $('.list_item')
var list = []
$.each(items, function( key, item ) {
  var movieName = $('h4 a', item).text()
  var movieShowTime = $('h4 span', item).text()
  var movie = {
    name: movieName,
    showTime: movieShowTime
  }
  list.push(movie)
})
  1. list输出到端点:
ctx.type = 'json'
ctx.body = {
  list: list
}

您应该在localhost:4000/public/movies处看到以下 JSON 格式的类似电影列表:

{
  "status": 200,
  "data": {
    "list": [{
      "name": " Onward (2020)",
      "showTime": ""
    }, {
      "name": " Finding the Way Back (2020)",
      "showTime": ""
    },
    ...
    ...
    ]
  }
}

You can find these examples in /chapter-13/jsdom/ in our GitHub repository. For more information about this npm package, visit https://github.com/jsdom/jsdom.

您可以看到这个工具在服务器端是多么有用。它使我们能够像在客户端一样操作原始 HTML。现在让我们转到 AVA,在下一节中学习它的一些基本用法,然后再将其与 Nuxt 应用中的jsdom一起使用。

艾娃

简而言之,AVA(不是 AVA 或 AVA,发音为/ˈeɪvə/)是 Node.js 的 JavaScript 测试运行程序。这里有很多测试者:摩卡咖啡、茉莉花、磁带等等。AVA 是现有列表的另一种替代方案。首先,AVA 很简单。设置起来真的很容易。此外,默认情况下,它并行运行测试,这意味着您的测试将运行得很快。它适用于前端和后端 Javascript 应用。总而言之,这确实值得一试。让我们按照以下步骤创建一个简单而基本的 Node.js 应用:

  1. 通过 npm 安装 AVA 并将其保存到package.json文件中的devDependencies选项:
$ npm i ava --save-dev
  1. 安装 Babel core 和其他 Babel 软件包,以便我们在应用测试中编写 ES6 代码:
$ npm i @babel/polyfill
$ npm i @babel/core --save-dev
$ npm i @babel/preset-env --save-dev
$ npm i @babel/register --save-dev
  1. package.json文件中配置test脚本如下:
// package.json
{
  "scripts": {
    "test": "ava --verbose",
    "test:watch": "ava --watch"
  },
  "ava": {
    "require": [
      "./setup.js",
      "@babel/polyfill"
    ],
    "files": [
      "test/**/*"
    ]
  }
}
  1. 使用以下代码在根目录中创建一个setup.js文件:
// setup.js
require('@babel/register')({
  babelrc: false,
  presets: ['@babel/preset-env']
})
  1. 创建以下类和函数,稍后我们将在应用中的这两个单独文件中对其进行测试:
// src/hello.js
export default class Greeter {
  static greet () {
    return 'hello world'
  }
}

// src/add.js
export default function (num1, num2) {
  return num1 + num2
}
  1. /test/目录中创建hello.js测试,用于测试/src/hello.js
// test/hello.js
import test from 'ava'
import hello from '../src/hello'

test('should say hello world', t => {
  t.is('hello world', hello.greet())
})
  1. 再次在/test/目录下的单独文件中创建另一个测试,用于测试/src/add.js
// test/add.js
import test from 'ava'
import add from '../src/add'

test('amount should be 50', t => {
  t.is(add(10, 50), 60)
})
  1. 在终端上运行所有测试:
$ npm run test

您还可以使用--watch标志运行测试,以启用 AVA 的监视模式:

$ npm run test:watch

如果测试通过,则应获得以下结果:

✓ add › amount should be 50
✓ hello › should say hello world

2 tests passed

You can find the preceding examples in /chapter-13/ava/ in our GitHub repository. For more information about this npm package, visit https://github.com/avajs/ava.

这很简单,很有趣,不是吗?看到我们的代码通过测试总是值得的。现在您对这个工具有了基本的了解,现在是在 Nuxt 应用中使用 jsdom 实现它的时候了。让我们在下一节讨论它。

使用 jsdomn 和 AVA 为 Nuxt 应用编写测试

您已经独立地学习了 jsdom 和 AVA,并做了一些简单的测试。现在,我们可以将这两个包合并到我们的 Nuxt 应用中。让我们使用以下步骤将它们安装到您在上一章中创建的 Nuxt 应用中/chapter-12/nuxt-universal/cross-domain/jwt/axios-module/frontend/

  1. 通过 npm 安装这两个工具并保存到package.json文件中的devDependencies选项:
$ npm i ava --save-dev
$ npm i jsdom --save-dev
  1. 安装 Babel core 和其他 Babel 软件包,以便我们在应用的测试中编写 ES6 代码:
$ npm i @babel/polyfill
$ npm i @babel/core --save-dev
$ npm i @babel/preset-env --save-dev
$ npm i @babel/register --save-dev
  1. 将 AVA 配置添加到package.json文件中,如下所示:
// package.json
{
  "scripts": {
    "test": "ava --verbose",
    "test:watch": "ava --watch"
  },
  "ava": {
    "require": [
      "./setup.js",
      "@babel/polyfill"
    ],
    "files": [
      "test/**/*"
    ]
  }
}
  1. 在根目录中创建一个setup.js文件,就像您在上一节中所做的那样,但使用以下代码:
// setup.js
require('@babel/register')({
  babelrc: false,
  presets: ['@babel/preset-env']
})
  1. 为在/test/目录中编写测试准备以下测试模板:
// test/tests.js
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'

let nuxt = null

test.before('Init Nuxt.js', async t => {
  const rootDir = resolve(__dirname, '..')
  let config = {}
  try { config = require(resolve(rootDir, 'nuxt.config.js')) } 
   catch (e) {}
  config.rootDir = rootDir
  config.dev = false
  config.mode = 'universal'
  nuxt = new Nuxt(config)
  await new Builder(nuxt).build()
  nuxt.listen(5000, 'localhost')
})

// write your tests here...

test.after('Closing server', t => {
  nuxt.close()
})

测试将在localhost:5000(或您喜欢的任何端口)上运行。您应该在生产版本上进行测试,因此,如果您的应用是为服务器端和客户端开发的,请在config.dev键中关闭开发模式,并在config.mode键中使用universal。然后,确保在测试过程完成后关闭 Nuxt 服务器。

  1. 编写第一个测试来测试我们的主页,以确保在此页面上呈现了正确的 HTML:
// test/tests.js
test('Route / exits and renders correct HTML', async (t) => {
  let context = {}
  const { html } = await nuxt.renderRoute('/', context)
  t.true(html.includes('<p class="blue">My marvelous Nuxt.js 
   project</p>'))
})
  1. /about路由编写第二个测试,以确保我们在此页面上呈现了正确的 HTML:
// test/tests.js
test('Route /about exits and renders correct HTML', async (t) => {
  let context = {}
  const { html } = await nuxt.renderRoute('/about', context)
  t.true(html.includes('<h1>About page</h1>'))
  t.true(html.includes('<p class="blue">Something awesome!</p>'))
})
  1. /about页面编写第三个测试,通过jsdom服务器端的 DOM 操作,确保文本内容、类名和样式符合预期:
// test/tests.js
test('Route /about exists and renders correct HTML and style', 
async (t) => {

  function hexify (number) {
    const hexChars = 
     ['0','1','2','3','4','5','6','7','8','9','a','b',
      'c','d','e','f']
    if (isNaN(number)) {
      return '00'
    }
    return hexChars[(number - number % 16) / 16] + 
     hexChars[number % 16]
  }

  const window = await nuxt.renderAndGetWindow(
   'http://localhost:5000/about')
  const element = window.document.querySelector('.blue')
  const rgb = window.getComputedStyle(element).color.match(/\d+/g)
  const hex = '' + hexify(rgb[0]) + hexify(rgb[1]) + hexify(rgb[2])

  t.not(element, null)
  t.is(element.textContent, 'Something awesome!')
  t.is(element.className, 'blue')
  t.is(hex, '0000ff')
})

如果测试以npm run test通过,则应得到以下结果:

✓ Route / exits and renders correct HTML (369ms)
✓ Route /about exits and renders correct HTML (369ms)
✓ Route /about exists and renders correct HTML and style (543ms)

3 tests passed

您可以看到,在我们的第三个测试中,我们创建了一个hexify函数来将由Window.getComputedStyle方法计算的十进制代码(R,G,B)转换为十六进制代码。例如,在 CSS 样式中设置为color: white的颜色将得到rgb(255, 255, 255)。因此,您将获得0000ffrgb(0, 0, 255),应用必须将其转换为通过测试。

You can find these tests in /chapter-13/nuxt-universal/ava/ in our GitHub repository.

做得好。您已经为 Nuxt 应用编写了简单的测试。我们希望您发现用 Nuxt 编写测试既简单又有趣。测试的复杂性取决于要测试的内容。因此,首先了解要测试的内容是非常重要的。然后,您可以开始编写一个合理、有意义和相关的测试。

但是,使用 jsdom 和 AVA 测试 Nuxt 应用有一些限制,因为它不涉及浏览器。请记住,jsdom 用于在服务器端将原始 HTML 转换为 DOM,因此我们使用 async/await 语句为前面的测试异步请求页面。如果你想使用浏览器来测试你的 Nuxt 应用,夜视是一个很好的解决方案,所以我们将在下一节介绍它。让我们继续。

介绍夜表

Nightwatch 是一个自动化测试框架,为基于 web 的应用提供端到端测试解决方案。它在后台使用 W3C WebDriver API(以前称为 Selenium WebDriver)打开web 浏览器对 DOM 元素执行操作和断言。如果你想使用浏览器测试你的 Nuxt 应用,这是一个很好的工具。但是在 Nuxt 应用中使用它之前,让我们在以下步骤中单独使用它来编写一些简单的测试,以便您对它的工作原理有一个基本的了解:

  1. 通过 npm 安装 Nightwatch 并将其保存到package.json文件中的devDependencies选项:
$ npm i nightwatch --save-dev
  1. 通过 npm 安装 GeckoDriver,并将其保存到package.json文件中的devDependencies选项:
$ npm install geckodriver --save-dev

Nightwatch 依赖于 WebDriver,因此我们需要根据您的目标浏览器安装特定的 WebDriver 服务器–例如,如果您只想针对 Firefox 编写测试,则需要安装 GeckoDriver。

在本书中,我们将重点介绍针对单个浏览器编写测试。但如果您想同时针对多个浏览器,如 Chrome、Edge、Safari 和 Firefox,则需要安装Selenium 独立服务器(也称为 Selenium Grid),如下所示:

$ npm i selenium-server --save-dev

Note that we will be testing on Firefox and Chrome in this book, so this selenium-server package will not be used.

  1. nightwatch添加到package.json文件中的test脚本中:
// package.json
{
  "scripts": {
    "test": "nightwatch"
  }
}
  1. 创建一个nightwatch.json文件来配置夜视,如下所示:
// nightwatch.json
{
  "src_folders" : ["tests"],

  "webdriver" : {
    "start_process": true,
    "server_path": "node_modules/.bin/geckodriver",
    "port": 4444
  },

  "test_settings" : {
    "default" : {
      "desiredCapabilities": {
        "browserName": "firefox"
      }
    }
  },

  "launch_url": "https://github.com/lautiamkok"
}

在这个简单的练习中,我们想在一个名为Lau Tiam Kok的特定贡献者上测试 github.com 的存储库搜索功能,因此我们在这个配置中的launch_url选项中设置了https://github.com/lautiamkok

我们将在/tests/目录中编写测试,因此我们在src_folders选项中指示目录位置。我们将只在4444(服务器端口)对 Firefox 进行测试,因此我们在webdrivertest_settings选项中设置了此信息。

You can find the options for the rest of test settings such as output_folder at https://nightwatchjs.org/gettingstarted/configuration/. If you want to find out the test settings for the Selenium Server, please visit https://nightwatchjs.org/gettingstarted/configuration/selenium-server-settings.

  1. 在项目根目录中创建一个nightwatch.conf.js文件,用于将驱动程序路径动态设置为服务器路径:
// nightwatch.conf.js
const geckodriver = require("geckodriver")
module.exports = (function (settings) {
  settings.test_workers = false
  settings.webdriver.server_path = geckodriver.path
  return settings
})(require("./nightwatch.json"))
  1. /tests/目录下的.js文件中(例如demo.js中)准备以下夜视测试模板,如下所示:
// tests/demo.js
module.exports = {
  'Demo test' : function (browser) {
    browser
      .url(browser.launchUrl)
      // write your tests here...
      .end()
  }
}
  1. /tests/目录下创建一个github.js文件,代码如下:
// tests/github.js
module.exports = {
  'Demo test GitHub' : function (browser) {
    browser
      .url(browser.launchUrl)
      .waitForElementVisible('body', 1000)
      .assert.title('lautiamkok (LAU TIAM KOK) · GitHub')
      .assert.visible('input[type=text][placeholder=Search]')
      .setValue('input[type=text][placeholder=Search]', 'nuxt')
      .waitForElementVisible('li[id=jump-to-suggestion-
        search-scoped]', 1000)
      .click('li[id=jump-to-suggestion-search-scoped]')
      .pause(1000)
      .assert.visible('ul[class=repo-list]')
      .assert.containsText('em:first-child', 'nuxt')
      .end()
  }
}

在本测试中,我们希望断言存储库搜索功能按预期工作,因此我们需要确保某些元素和文本内容存在且可见,例如<body><input>元素,以及nuxtlautiamkok (LAU TIAM KOK) · GitHub的文本。当您使用npm run test运行它时,您应该得到以下结果(假设测试通过):

[Github] Test Suite
===================
Running: Demo test GitHub

✓ Element <body> was visible after 34 milliseconds.
✓ Testing if the page title equals "lautiamkok (LAU TIAM KOK) · 
   GitHub" - 4 ms.
✓ Testing if element <input[type=text][placeholder=Search]> is 
   visible - 18 ms.
✓ Element <li[id=jump-to-suggestion-search-scoped]> was visible 
   after 533 milliseconds.
✓ Testing if element <ul[class=repo-list]> is visible - 25 ms.
✓ Testing if element <em:first-child> contains text: "nuxt"
  - 28 ms.

OK. 6 assertions passed. (5.809s)

You can find the preceding test in /chapter-13/nightwatch/ in our GitHub repository. For more information on Nightwatch, please visit https://nightwatchjs.org/.

与 AVA 相比,Nightwatch 并不是最简单的,因为它需要一些长而复杂的配置,但是如果您遵循最简单的nightwatch.json文件,它应该可以让您很快开始使用 Nightwatch。那么,让我们将您在本节学到的知识应用到下一节的 Nuxt 应用中。

使用 Nightwatch 为 Nuxt 应用编写测试

在本练习中,我们想针对Chrome 浏览器测试我们在上一章第 12 章创建用户登录和 API 认证中创建的用户登录认证。我们希望确保用户可以使用其凭据登录,并按预期获取其用户数据。我们将在存放 Nuxt app 的/frontend/目录中编写测试,因此我们需要相应地修改package.json文件,并按照以下步骤编写测试:

  1. 通过 npm 安装 ChromeDriver 并将其保存到package.json文件中的devDependencies选项:
$ npm install chromedriver --save-dev
  1. 将启动 URL 更改为localhost:3000和其他设置,如nightwatch.json文件中的以下代码块所示,用于对夜视配置文件中的 Chrome 进行测试:
// nightwatch.json
{
  "src_folders" : ["tests"],

  "webdriver" : {
    "start_process": true,
    "server_path": "node_modules/.bin/chromedriver",
    "port": 9515
  },

  "test_settings" : {
    "default" : {
      "desiredCapabilities": {
        "browserName": "chrome"
      }
    }
  },

  "launch_url": "http://localhost:3000"
}
  1. 在项目根目录中创建一个nightwatch.conf.js文件,用于将驱动程序路径动态设置为服务器路径:
// nightwatch.conf.js
const chromedriver = require("chromedriver")
module.exports = (function (settings) {
  settings.test_workers = false
  settings.webdriver.server_path = chromedriver.path
  return settings
})(require("./nightwatch.json"))
  1. /tests/目录下创建一个login.js文件,代码如下:
// tests/login.js
module.exports = {
  'Local login test' : function (browser) {
    browser
      .url(browser.launchUrl + '/login')
      .waitForElementVisible('body', 1000)
      .assert.title('nuxt-e2e-tests')
      .assert.containsText('h1', 'Please login to see the 
       secret content')
      .assert.visible('input[type=text][name=username]')
      .assert.visible('input[type=password][name=password]')
      .setValue('input[type=text][name=username]', 'demo')
      .setValue('input[type=password][name=password]', 
       '123123')
      .click('button[type=submit]')
      .pause(1000)
      .assert.containsText('h2', 'Hello Alexandre!')
      .end()
  }
}

此测试的逻辑与上一节中的测试相同。我们希望确保在登录之前和之后登录页面上都存在某些元素和文本。

  1. 在运行测试之前,在您的终端上运行位于localhost:3000localhost:4000的 Nuxt 和 API 应用,然后在/frontend/目录下打开另一个带有npm run test的终端。如果测试通过,则应获得以下结果:
[Login] Test Suite
==================
Running: Local login test

✓ Element <body> was visible after 28 milliseconds.
✓ Testing if the page title equals "nuxt-e2e-tests" - 4 ms.
✓ Testing if element <h1> contains text: "Please login to see the 
   secret content" - 27 ms.
✓ Testing if element <input[type=text][name=username]> is 
   visible - 25 ms.
✓ Testing if element <input[type=password][name=password]> is 
   visible - 25 ms.
✓ Testing if element <h2> contains text: "Hello Alexandre!" 
  - 75 ms.

OK. 6 assertions passed. (1.613s)

Note that you must run the Nuxt app and the API concurrently before running the tests.

You can find the preceding test in /chapter-13/nuxt-universal/nightwatch/ in our GitHub repository.

做得好。您已经完成了关于为 Nuxt 应用编写测试的这一简短章节。本章中的步骤和练习为您提供了扩展测试的基础,因为应用变得越来越庞大和复杂。让我们在最后一节总结一下您在本章学到的知识。

总结

在本章中,您学习了使用 jsdom 进行服务器端 DOM 操作,以及分别使用 AVA 和 Nightwatch 编写简单测试,然后尝试将这些工具结合使用,在我们的 Nuxt 应用上运行端到端测试。您还了解了端到端测试和单元测试之间的区别及其各自的优缺点。最后但并非最不重要的一点是,您已经从本章的练习中了解到,默认情况下,Nuxt 的配置非常完美,您可以轻松地使用 jsdom 和 AVA 编写端到端测试。

在下一章中,我们将介绍如何使用诸如 ESLint、Prettier 和 StandardJS 之类的 linter 保持代码整洁,并将它们集成和混合到 Vue 和 Nuxt 应用中。最后,您将学习 Nuxt 部署命令,并使用它们将应用部署到实时服务器。所以,请继续关注。