教程:用 Jest 与 Puppeteer 进行 UI test

原文地址:https://www.valentinog.com/blog/ui-testing-jest-puppetteer/#jest-puppeteer-visual-debug

原文作者: Valentino Gagliardi

自从 Puppeteer 这个库出现以来,我就开始考虑用 Jest 和 Puppeteer 进行测试了。 Puppeteer 有很多有意思的 API。

Banner

接下来的文章我将会以联系人表单为例,简单的讲解如何对其进行 UI Test

测试的技术选型,我们会使用 Jest 和 Puppeteer。

昨天在写测试的时候,刚好看到了 Kent C.Dodds 写的文章。

让 UI 测试脚本更耐用一些”这篇文章里,阐述了怎么样通过使用 data-* 属性来让 UI 测试脚本更不容易失效。

data-* 属性可以在几乎任何 HTML 元素上定义一个 data 属性。当需要利用 JavaScript 操作 HTML 进行数据交换的时候,这个属性特别有用。

Kent 的文章来的很是时候,因为当时我恰好也要写类似的东西了。

1
2
3
await page.waitForSelector("#contact-form");
await page.click("#name");
await page.type("#name", user.name);

尽管我不太喜欢用自定义 data-* 这种方法来进行测试,但是不得不承认,这是一个好方法。不管你的程序是大型工程还是小型项目,这个方法都适用,但是在文章里,我还是用比较经典的元素选择的例子来写一下 demo。

号外,你对持续集成感兴趣么?感兴趣的话可以看一下 Cypress >> 用 Cypress 进行更好的 JavaScript E2E 测试

用 Jest 与 Puppeteer 进行 UI 测试: 测试一个联系人表单

我的目标是测试一个联系人表单。

如下:

联系人表单

它有如下元素:

  1. 一个输入名字的 input
  2. 一个输入 email 的 input
  3. 一个输入手机号的 input
  4. 一个 textarea
  5. 一个隐私设置的 checkbox
  6. 一个提交 button

如果我想测试这个联系人列表,我应该怎么做呢?

测试上面这个表单,意味着要断言用户可以提交一个网络请求

用 Jest 与 Puppeteer 进行 UI 测试: 安装工程

我们先来小窥一下要用到的工具。

puppeteer

Jest: Facebook 开源的一个测试框架。Jest 通过一个基本的断言库(Except)提供了一个自动化测试的平台。

Puppeteer: 一个用来控制 headless Chrome 的 Node.js 库。虽然这个库还很新,但是现在正是试试这个库是适于加入你的工作流的好时候。

Faker: 一个用于产生随机数据的 Node.js 库。姓名、电话号码、地址。这个库很像 PHP 里的那个 Faker 库。

如果你已经弄好工程目录了,可以用下面的命令来安装上述的库:

1
npm i jest puppeteer faker --save-dev

因为 Puppeteer 需要下载它自己所需要的版本的 Chromium,所以安装 Puppeteer 会花一些时间。

Chromium 是一个在 Chrome 之后的开源浏览器。Chromium 和 Chrome 几乎共享了所有的功能,两者的差异只在部分协议的细节上。

一旦安装完成了,你需要在 package.json 里配置一下 Jest。test 命令应该写成一个可执行的 Jest 命令:

1
2
3
"scripts": {
"test": "jest"
}

在 Jest 脚本里,别忘了引入 Puppeteer:

1
import puppeteer from "puppeteer";

为了能用 ES6 的语法,我们需要为 Jest 配置 Babel。用下面这个命令安装跟 Babel 相关的东西:

1
npm i babel-core babel-jest babel-preset-env --save-dev

安装完以后,在你的工程目录里创建一个文件名为 .babelrc 的文件:

1
2
3
{
"presets": ["env"]
}

上述这些都配置好后,我们就可以开始写一个简单的测试脚本了。

用 Jest 与 Puppeteer 进行 UI 测试: 写测试脚本

在你的工程目录下新建一个文件夹,可以命名为 test 或者 spec。然后在这个文件夹里建一个新的文件,名字是 form.spec.js

接下来我会把测试脚本拆成好几部分,把重点拿出来让你看看。在文章的末尾,我们能看到完整的代码。

按照顺序引入 Fake 和 Puppeteer:

1
2
import faker from "faker";
import puppeteer from "puppeteer";

配置表单的 URL(也许你想测试的是本地启动服务的开发版本,而不是真实的网站的生产版本):

1
const APP = "https://www.change-this-to-your-website.com/contact-form.html";

用 Faker 创建一个假的用户信息:

1
2
3
4
5
6
const lead = {
name: faker.name.firstName(),
email: faker.internet.email(),
phone: faker.phone.phoneNumber(),
message: faker.random.words()
};

给 Puppeteer 定义一些变量:

1
2
3
4
let page;
let browser;
const width = 1920;
const height = 1080;

定义 Puppeteer 应该表现出怎么样的行为:

1
2
3
4
5
6
7
8
9
10
11
12
beforeAll(async () => {
browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
page = await browser.newPage();
await page.setViewport({ width, height });
});
afterAll(() => {
browser.close();
});

beforeAllafterAll 都是 Jest 方法。简单来说就是,在开始跑测试以前,我们必须用 Puppeteer 启动一个浏览器核心,然后用 browser.newPage() 启动打开一个新的页面。

当测试用例完成了以后,浏览器核心必须通过 browser.close() 来进行关闭。

除了 beforeAllafterAll 以外,Jest 还有很多其他 API,详情可以看一下 Jest 文档。毕竟,整个测试只用一个浏览器来核心完成要比每个测试用例都重新启动和关闭浏览器核心要方便的多。

上面的代码里有一些需要注意的地方:

我在代码里用了 headless: false 来启动(lanuch)浏览器核心,以便把 Chromuim 真实的显示出来。这其实只是因为我需要录像来给你展示测试是如何工作的而已,并不是测试必须这样写。

在实际测试过程中,并不需要打开一个实际的浏览器页面,所以到时候,只需要把跟 lanuch 方法相关的代码删除掉就可以了。

setViewPort() 也同样,可以删掉了。你也可以设置根据不同的环境来进行不同的 lanuch 操作,比如在开发环境中禁用 headless 模式以便可视化调试,在跑测试的时候只启动核心而不显示窗口。点这里去了解一下怎么做

现在我们可以写实际的测试脚本了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe("Contact form", () => {
test("lead can submit a contact request", async () => {
await page.goto(APP);
await page.waitForSelector("[data-test=contact-form]");
await page.click("input[name=name]");
await page.type("input[name=name]", lead.name);
await page.click("input[name=email]");
await page.type("input[name=email]", lead.email);
await page.click("input[name=tel]");
await page.type("input[name=tel]", lead.phone);
await page.click("textarea[name=message]");
await page.type("textarea[name=message]", lead.message);
await page.click("input[type=checkbox]");
await page.click("button[type=submit]");
await page.waitForSelector(".modal");
}, 16000);
});

注意,上面的代码里在 Jest 里使用 async/await 了。即假设你在用最新版本的 Node.js 了。

我们来看看当使用 Jest 和 Puppeteer 的时候,headless Chrome 都做了些什么:

  1. 打开 APP 里写的那个链接;
  2. 等待联系人列表的 UI 渲染;
  3. 点击、填入表单项;
  4. 点击 checkbox;
  5. 提交表单;
  6. 等待 .modal 这个 DOM 元素出现。

注意:注意以第二参数形式在 test() 方法里传递给 Jasmine 的超时时间(16000)。当你像看 Chrome 浏览器跟页面的交互的时候,这个设置是很有必要的。

如果不是 headless 模式,并且没有配置超时时间的话,会得到如下的错误:

1
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

总之,当 Chrome 运行在 headless 模式的时候,的确可以移除超时设置的,只是会报错。

然后用下面的命令来执行测试:

1
npm test

然后你就可以看见下面这样的魔法了:

用 Jest 与 Puppeteer 进行测试:测试更多的部分

ok,现在联系人表单已经测试好了,我可以继续测试这个页面里其他部分的内容了。

每个网页,应该都有一个有意义的标题对吧,那么怎么测试呢?

下面的代码是用来测试 <title></title> 是否正确的:

1
2
3
4
5
6
7
8
9
describe("Testing the frontend", () => {
test("assert that <title> is correct", async () => {
const title = await page.title();
expect(title).toBe(
"Gestione Server Dedicati | Full Managed | Assistenza Sistemistica"
);
});
// 在下面你可以添加更多的测试用例!
});

导航栏呢?页面里至少应该有一个导航栏吧!

用 Jest 与 Puppeteer 测试导航栏是否存在:

1
2
3
4
5
6
//
test("assert that a div named navbar exists", async () => {
const navbar = await page.$eval(".navbar", el => (el ? true : false));
expect(navbar).toBe(true);
});
//

或者测试一下,某个特定的元素里是否有期望的文本信息:

1
2
3
4
5
6
//
test("assert that main title contains the correct text", async () => {
const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent);
expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed");
});
//

那么有关 SEO 应该怎么测试呢?

用 Jest 与 Puppeteer 测试一下 SEO 关注的重点,比如一个友情链接是否存在:

1
2
3
4
5
6
7
describe("SEO", () => {
test("canonical must be present", async () => {
await page.goto(`${APP}`);
const canonical = await page.$eval("link[rel=canonical]", el => el.href);
expect(canonical).toEqual("https://www.servermanaged.it/");
});
});

当然,还可以测试更多的内容。

在那天写完 UI 测试后,看见那些绿色的标记,我很开心:
测试用例通过情况

Puppeteer 给了你无限的可能。现在有很多新的测试框架正在基于 Puppeteer 进行开发。当然,API 还会继续改善,但是要知道,基础条件是必不可少的。

并且 Puppeteer 还能和 Jest 很好的结合在一起。

用 Jest 与 Puppeteer 进行测试:下一步

可能你会觉得 Puppeteer 本身或者 Puppeteer 的 API 并不够方便。我也懂你的感受。

这个库还很新,但是现在是检验这个库是否适合融入你的工作流的好时候。

Puppeteer 仍然在开发阶段中,并且接下来会有很多的改善。与此同时,你可以看看那些用了 Cypress 的例子。

你是怎么样测试你的程序的呢?有很多人在用 Puppeteer 做 E2E 测试。你呢?


附录:用 Jest 与 Puppeteer 进行测试时的可视化 debug

Puppeteer 在使用的时候,你可以选择 Chromium 的 headless非 headless 两种模式。

就像我们之前看到的那样:

1
2
3
4
5
6
7
8
9
10
beforeAll(async () => {
browser = await puppeteer.launch({
// Debug mode !
headless: false,
slowMo: 80,
args: [`--window-size=1920,1080`]
});
page = await browser.newPage();
///
});

如果你想进行可视化的 debug,那就必须要给 Jasmine 设置一个超时时间用来启动浏览器核心。否则测试会很快的中止掉,因为异步请求全部被忽略掉了。这个超时时间被定义为 test() 函数的第二个参数。

1
2
3
4
5
6
7
8
9
describe("Contact form", () => {
test(
"lead can submit a contact request",
async () => {
///// some assertions
},
16000 // <<< Jasmine timeout
);
});

但是当在自动化测试环境的时候,就不需要真的启动浏览器窗口了。如果你启动了,这将会导致这个测试永远的持续下去。所以怎么样在 headless 模式和普通模式之间轻松的进行切换呢?

写点辅助函数,把这些函数放在文件 testingInit.js 里:

1
2
3
4
5
6
7
8
9
10
11
export const isDebugging = () => {
let debugging_mode = {
puppeteer: {
headless: false,
slowMo: 80,
args: [`--window-size=1920,1080`]
},
jasmine: 16000
};
return process.env.NODE_ENV === "debug" ? debugging_mode : false;
};

然后你就可以在你的测试脚本里引用这个辅助函数了:
先是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
///
import { isDebugging } from "./testingInit.js";
///
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< 可视化模式
page = await browser.newPage();
///
});

/// 一些脚本

describe("Contact form", () => {
test(
"lead can submit a contact request",
async () => {
///// 一些断言
}, isDebugging().jasmine // <<< Jasmine timeout
);

然后你就可以用命令分别用这两种模式来启动了,如果你想用 headless 模式启动的话:

1
npm test

下面是如何用 debug 模式启动:

1
NODE_ENV=debug npm test

感谢阅读!

图片来源:https://unsplash.com/@shotbyjames