65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Azure Pipelines 中使用 Nightwatch.js 进行端到端测试

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (7投票s)

2019年3月28日

CPOL

12分钟阅读

viewsIcon

16925

downloadIcon

160

如何使用 TypeScript 和 Nightwatch.js 框架在 Azure Pipeline 中运行端到端测试

Azure Pipelines End-To-End using Nightwatch.js

目录

引言

去年,微软决定将其 Team Foundation ServiceVisual Studio OnlineVisual Studio Team Services 进行品牌重塑:Azure DevOps 应运而生。其中一项重要的营销工具是引入了一些子产品(以前称为实际产品的功能),例如 Azure Boards(管理问题)或 Azure Repos(源代码存储库)。另一个是 Azure Pipelines,它包含构建作业和发布定义。这是微软提供的 CI/CD 服务。

使用 Azure Pipelines 的一个非常酷的卖点是“免费使用”的宣传。简而言之,它允许我们为自己的项目使用多达 10 个并行运行的构建作业。我想我不用多说,这绝对是一个很好的提议。事实上,市面上有很多 CI/CD 提供商(为开源项目提供免费服务),但没有一个能为我们(非开源项目)提供如此强大的能力。

Azure Pipelines description

我们实际上可以利用这些免费计算能力来为自己建立各种有用的东西,例如,调度一个清理作业以在特定时间间隔运行。

另一种可能性是使用 Azure Pipelines 进行自动化端到端测试。我们会发现,默认的代理,即运行构建作业的配置,已经 集成了 Firefox 和 Chrome 等浏览器(Linux)。在其他托管代理上,我们甚至可以看到 Safari 或 Edge 的可用性。无论哪种情况,浏览器以及必要的驱动程序已经默认可用。

在本文中,我们将探讨一个已被证明在 Azure Pipelines 上运行自动化端到端测试时高效且易于使用的设置。我们将选择基于 Node.js 的 Nightwatch.js 框架,并结合 TypeScript 使用它。

背景

运行端到端 (E2E) 测试是保证应用程序健壮性的重要环节。当然,E2E 测试永远无法取代单元测试,但它可以确保至少对于我们定义的标准用户画像来说,用户可触及的流程似乎是没问题的。

虽然像 Selenium 这样的浏览器自动化工具已经存在了很长时间,但最近在创建所谓的“无头”浏览器方面投入了更多精力。大多数情况下,这些并不是独立的浏览器,而是运行标准浏览器的特殊模式,例如,以无头模式运行 Chrome。无头模式为我们提供了更轻量级的实例(在操作资源和运行软件所需的依赖方面)。此外,这一整个方向得到了标准化浏览器自动化 API 的支持。

所谓的 WebDriver API(是的,这是一个官方的 W3C 标准),最终将被所有主要的浏览器供应商支持。目前,Firefox 和 Chrome 的支持还可以,而 Chrome 大部分仍然使用前身称为“JsonWireProtocol”的协议。从长远来看,这将使 Selenium 过时,运行完全自动化和无人值守的 UI E2E 测试所需的资源会更少。

虽然我们可以直接针对 WebDriver API(或其他 API,例如 Selenium)进行操作,但我们肯定希望在编写实际测试时获得更多便利。在我看来,使用 Node.js / JavaScript 进行 Web 测试是有意义的。在这个领域中,Nightwatch.js 是冉冉升起的新星之一。

将 Nightwatch.js 与 TypeScript 结合使用

Nightwatch.js 非常直接。如果我们想要获得类型补全,我们应该使用 TypeScript 并导入相应的接口,例如 NightwatchBrowser

框架本身由多个部分组成。核心是测试。一个测试可以使用所有其他部分来使维护和重用变得相当容易。在这里,我们发现“命令”,它们扩展了 browser 对象(用于自动化浏览器操作、执行断言和期望)的 API 表面。

接下来我们发现“断言”。断言是比较预期状态和实际状态的基础。它也可以使用内部命令,并可以访问完整的浏览器表面。最后,我们有“页面对象”,它们使得页面中的常量(如重复的(CSS)选择器、URL 或其他数据)可重用。

在我们深入研究如何添加测试之前,让我们先添加命令和断言。

添加自定义命令

在我们的源文件夹(例如,名为 src)中,我们应该创建另一个名为 commands 的子文件夹。我们将为我们编写的每个命令使用此文件夹。文件名非常重要 - Nightwatch.js 将使用文件名来标记命令。因此,像 foo.ts(稍后转译为 foo.js)这样的文件名将可以通过 browser.foo() 访问。

命令始终是单个文件(一个 Node.js 模块),它导出 `command` 函数。如前所述,当我们通过 Nightwatch 的 browser API 访问它时,该函数将根据其文件名进行命名。

以下示例命令创建了一个 `compareScreenshot` 命令。它使用了现有的 `saveScreenshot` 命令和一个自定义断言,该断言是附加代码的一部分,但不是文章的一部分。

import { NightwatchBrowser } from 'nightwatch';

export function command(this: NightwatchBrowser, filename: string, 
                        tolerance = 0, callback?: Function) {
  const screenshotPath = 'screenshots/';
  const resultPath = `${screenshotPath}results/${filename}`;

  return this.saveScreenshot(resultPath, () => {
    this.assert.screenshotEquals(filename, tolerance, result => {
      if (typeof callback === 'function') {
        callback.call(this, result);
      }
    });
  });
}

重要的是,我们返回调用 `saveScreenshot` 的结果,它本身在某个时候返回 `this`,即 `NightwatchBrowser` 实例。这对于遵循链式调用概念很重要。稍后,当我们创建一个示例测试时,我们将看到这种流畅的测试定义是如何方便的。

我们遗漏的一件重要事情是 TypeScript 定义。由于命令被魔术般地添加到 Nightwatch 提供的 API 中,我们将无法获得任何代码补全。但是,通过编写一些 d.ts 文件,我们可以利用 TypeScript 的接口合并功能。

import * as NW from 'nightwatch';

// merge interfaces with nightwatch types
declare module 'nightwatch' {
  // ...
  export interface NightwatchCustomCommands {
    compareScreenshot(
      this: NW.NightwatchBrowser,
      filename: string,
      tolerance?: number,
      callback?: Function,
    ): NW.NightwatchBrowser;
  }
}

这将使我们的代码了解我们自己的命令,并允许我们在使用 Nightwatch.js 时获得完整的 IDE / 类型检查支持 - 尽管使用了自定义命令。

添加自定义断言

自定义命令很有用,毕竟,我们需要为我们的 E2E 测试提供简洁的指令。然而,如果不能运行测试来断言行为,所有命令都将毫无用处。

Nightwatch.js 提供了三种不同的可能性。我们发现验证 (verify)、期望 (expects) 和断言 (assert)。它们之间有细微的差别(例如,一个会继续测试,而另一个则不会),但只有最后一类可以扩展。

自定义断言的创建方式与自定义命令非常相似。我们需要将它们放在一个专用文件夹中,并为每个自定义断言编写一个模块(即文件)。文件名决定了自定义断言的名称,而断言模块的导出需要导出一个名为 `assertion` 的函数。

让我们编写一个非常简单的断言,看看一个 URL(例如,在点击链接元素后)是否与正则表达式匹配。

import { format } from 'util';

export function assertion(regex: RegExp, msg?: string) {
  this.message = msg || format('Testing if the URL match the regex "%s".', regex);
  this.expected = regex;

  this.pass = function(value) {
    return this.expected.test(value);
  };

  this.value = result => result.value;

  this.command = function(callback) {
    return this.api.url(callback);
  };

  return this;
}

自定义断言函数需要包含三个部分:一个 `pass` 函数(何时算作通过?)、一个计算调用命令时找到的结果值的函数,以及最后调用哪个命令来将网站置于可以执行断言的状态。

与命令一样,我们需要扩展基础的 Nightwatch.js 类型定义。否则,`assert` 属性只会显示已内置的断言。

我们再次将扩展存储在与自定义命令相同的 d.ts 文件(可能是同一个 d.ts 文件)中。

import * as NW from 'nightwatch';

// merge interfaces with nightwatch types
declare module 'nightwatch' {
  export interface NightwatchCustomAssertions {
    urlMatch(this: NW.NightwatchBrowser, regex: RegExp, msg?: string): 
             NW.NightwatchBrowser;
  }
  
  // ...
}

将命令和断言分开非常重要。断言的输出不仅用于为运行器填充失败或成功的决策,还将由编写报告文件的报告器使用(默认使用 JUnit XML 格式)。

在 Azure Pipeline 中运行 Nightwatch.js 的配置

现在我们对 Nightwatch.js 有了初步了解,是时候在 Azure Pipeline 中实际运行它了!让我们从 Nightwatch 的配置开始。

可用的 Package.Json 脚本

开箱即用,Nightwatch 就可以运行了。以下依赖项对于运行它都是必需的(`mkdirp` 只有在我们考虑创建新目录时才相关,例如用于存储屏幕截图,`node-resemble-js` 将是进行屏幕截图比较/差异分析所必需的)。

{
  // ...
  "dependencies": {
    "chromedriver": "^2.46.0",
    "mkdirp": "^0.5.1",
    "nightwatch": "^1.0.19",
    "node-resemble-js": "^0.2.0"
  },
}

长话短说:如果该系统上安装了 Chrome,我们就可以运行它!

让我们在 package.json 文件中为方便起见定义一些额外的脚本。实际运行此程序需要进行转译(因为我们想使用 TypeScript)并运行 Nightwatch CLI。除此之外,我们可能希望在不同的环境中运行(通过使用 `--environment` 或 `-e` 标志调用 Nightwatch CLI),因此为所有已知环境添加更多脚本是有意义的。

以下部分展示了一个示例配置。

{
  // ...
  "scripts": {
    "start": "npm run build && npm run test",
    "test:ci": "npm run build && nightwatch -e default",
    "test:local": "npm run build && nightwatch -e local",
    "build": "tsc --project tsconfig.json",
    "test": "nightwatch"
  },
  // ...
}

现在我们已经正确配置了应用程序,我们还需要配置 Nightwatch 本身。

基础 Nightwatch 配置

到目前为止,所有这些脚本都很好,但 Nightwatch 还不(尚未)知道从哪里获取测试、命令和断言等。此外,我们还没有指定我们要与哪个浏览器通信以及这种通信是什么样的。

以下 nightwatch.json 包含最重要的部分。请注意,我们总是针对 dist 文件夹而不是 src 文件夹,因为 Nightwatch 只理解 JavaScript 而不理解 TypeScript。

{
  "src_folders" : ["dist/tests"],
  "output_folder" : "./reports",

  "custom_assertions_path": "./dist/asserts",
  "custom_commands_path": "./dist/commands",
  "globals_path" : "./dist/globals.js",

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

  "test_settings" : {
    "default" : {
      "desiredCapabilities": {
        "browserName": "chrome",
        "javascriptEnabled": true,
        "acceptSslCerts": true,
        "chromeOptions": {
          "prefs": {
            "intl.accept_languages": "en-US,en"
          },
          "args": [
            "--headless"
          ]
        }
      },
      "skip_testcases_on_fail": false,
      "globals": {
        // global variables here
      }
    }
  }
}

虽然我们可以使用多个浏览器,但在这个样板代码中我们只使用 Chrome。我们将其设置为始终使用英语(如果您想测试本地化,可以在测试中本地覆盖它,或者回退到始终本地设置)和无头模式。使用无头模式,我们将在托管代理上无法进行操作。

重要的是,我们也没有配置跳过测试用例。通常,如果一个测试模块中的一个测试用例失败,所有剩余的测试模块也会被跳过。尤其是在不太断开连接的测试模块中,不应立即关机。

添加一个简单的测试

编写测试就像创建模块和添加不同的导出函数一样简单明了。

以下代码片段创建了两个测试,用于验证对某个示例主页的登录成功/失败。我们使用了名为 login 的自定义命令。

import { NightwatchBrowser } from 'nightwatch';

module.exports = {
  'The login works with the correct credentials'(browser: NightwatchBrowser) {
    return browser
      .login()
      .assert.containsText('#case_login > .success', 'WELCOME')
      .end();
  },

  'The login fails with the incorrect credentials'(browser: NightwatchBrowser) {
    return browser
      .login({ pass: 'foo' })
      .assert.containsText('#case_login > .error', 'DENIED')
      .end();
  },
};

在本地使用 Nightwatch.js 运行 E2E 测试如下所示

Running E2E Tests with Nightwatch.js locally

此外,我们甚至可以编写一个测试来检查给定的设计。

import { NightwatchBrowser } from 'nightwatch';

module.exports = {
  'Design of homepage'(browser: NightwatchBrowser) {
    return browser
      .login()
      .compareScreenshot('design-of-homepage.png')
      .end();
  },

  beforeEach(browser: NightwatchBrowser) {
    return browser.windowSize('current', 1000, 1000);
  },
};

beforeEach 是一个特殊函数,在每个测试开始之前调用,但在浏览器为测试设置好之后。因此,这是为模块中的所有测试设置浏览器使用方式的好方法。

对于屏幕截图,固定视觉边界条件非常重要。在这种情况下,以便看到我们想看的所有内容,并获得可重现的结果。

比较设计如下

  • 如果 screenshots/baseline 目录中不存在具有给定名称的文件,则会创建它。
  • 新的屏幕截图将记录在 screenshots/results 目录中。
  • 记录的屏幕截图与基线屏幕截图之间的差异(如果找到)将捕获在 screenshots/diffs 目录中。

有一个可变的容差级别,该级别自动设置为 0(不容忍任何差异)。由于平台渲染差异,在这里设置更高的阈值(例如,容差 = 11)可能有用。

尤其是在我们想比较 macOS 和 Linux / Windows 之间的屏幕截图时,这一点尤其如此,但对于其他平台也同样适用。

Tolerance levels are important

上面的代码片段显示了 Azure DevOps 控制台中失败的测试的外观。然而,虽然我们可能需要在这里调整容差级别(Windows vs Linux),但也可能存在其他问题(例如,网站本身)。唯一知道的方法是获取记录的屏幕截图,这需要在我们的 Azure Pipeline 设置中进行考虑。

Azure Pipeline 设置

为了让我们的 E2E Pipeline 有成效,我们需要以下步骤(按顺序)

  • 在包含至少 Chrome 的托管 Ubuntu 代理上运行
  • 克隆存储库
  • 安装依赖项
  • 转译(即构建)TypeScript 文件以生成 JavaScript
  • 运行 Node.js 应用程序
  • 发布生成的测试结果

步骤 1 和 2 是隐含的。对于步骤 4 和 5,我们创建了一个单独的脚本。

以下 azure-pipelines.yml 使用我们 Nightwatch.js 样板代码的结构,一次性涵盖了所有这些内容。

pool:
  name: Hosted Ubuntu 1604
  demands: npm

steps:
- task: Npm@1
  displayName: 'Install Dependencies'
  inputs:
    verbose: false
- task: Npm@1
  displayName: 'Build and Run'
  inputs:
    command: custom
    verbose: false
    customCommand: start
- task: PublishTestResults@2
  displayName: 'Publish Test Results'
  inputs:
    testResultsFiles: 'reports/*.xml'
    mergeTestResults: true
    failTaskOnFailedTests: true
  condition: succeededOrFailed()

此设置也可以通过图形界面完成。

The pipeline definition in the Azure DevOps client

重要的是,我们需要设置正确的触发器。

在下面的示例中,我们只触发一次(在周四),但每次 master 分支更改时都会触发。

Triggers for running the E2E Tests

一旦测试成功运行,它们就会显示在构建详情中,并可以被 Azure DevOps 进一步使用/连接。

下面的截图显示了 Azure DevOps 中显示的收集的文本结果。这个视图的好处是我们能够检查每个运行过的文本。我们可以查看测试的历史记录,并获得测试报告的完整信息。我们甚至可以查看和下载附件。

Viewing the test results

如果我们拥有活跃的 Azure DevOps 测试管理器订阅,我们可以进一步连接这些结果,并将其与不同的测试套件、手动测试和更详细的描述对齐。

兴趣点

附带了一个小型示例项目。它将包含基本的样板代码和 CodeProject 本身的测试。由于自动 UI 测试对目标 Web 应用程序的任何(不仅是视觉上的)更改都相当敏感,因此我无法保证未来的测试仍然是绿色的。我希望从示例项目中能获得正确的思路。

您如何解决广泛的端到端测试问题?您最喜欢的可靠运行这些测试的基础设施是什么?在评论中告诉我!

历史

  • 2019年3月29日:v1.0.0 | 初始发布
  • 2019年4月1日:v1.1.0 | 添加了图像信息
  • 2019年4月14日:v1.2.0 | 添加了目录
© . All rights reserved.