软件开发流程中,软件测试是最重要的一个环节。为了规避或者发现软件的缺陷,经常需要大量测试人员进行手动测试,而手动测试中我们最常见的就是模拟点击,模拟输入,之后进行结果对比,就是我们常说的"点点点"。大家都知道手动执行这些操作是非常耗时的操作,在这篇文章中,我将带领大家使用 Puppeteer 将这些繁琐耗时的操作转化为自动化操作。
从开发者的角度来看, 测试,尤其是手动测试,似乎是一个不合理的负担,阻碍了软件开发的速度。虽然整个开发者社区以及大多数管理者正在逐渐理解,并且认为经过充分测试和运行的代码可以为用户创建一个优秀的产品,保证质量比增加又一个新功能更有价值,我们仍然认为手动 QA 是一个负担。
手动测试经常变成项目的负担。
但手动测试确实不是我们面临的问题,而是我们使用它的方式。最初的手动测试经常是在开发完成后进行功能上的校验,然后再将其发送给客户。你可以对产品进行单元测试,但是在真实应用场景中我们可能会在操作上与单元测试而不同。因此,我们尽可能使用手动测试模拟真实的用户操作场景。
在实践 DevOps 流程的过程中,我们意识到,每次提交一小段代码、频繁提交通常可以极大地减少 bug 的数量,使我们的产品能够更好地响应用户和市场需求,同时为我们自己创造一个良好的工作环境。在实现频繁发布的过程中,手动测试常常是一个很大的难点。因为不管出于什么原因,组织需要为每个版本运行一遍完整的手工测试。这使得新功能的测试会有滞后,以便 QA 可以一次处理尽可能多的之前版本出现的已知 Bug。如果我们忽略了这一点,一旦如果出现 Bug,那么很难跟踪几十次提交中哪一次引入了 Bug。这样通常会阻塞开发几天,只为了在一个版本上重现这个 bug。我们发明了一些复杂的过程,比如一个星期的代码冻结,以创建一个 “稳定的测试环境”,但实际上,它往往并不稳定。
编程和其他行业一样需要 QA。
尽管有这些问题,我们仍然像其他行业一样需要手工 QA。但现在是时候转移注意力了。当我们查看其他行业时,我们总是会看到手动测试,它只是让人放心地认为一切都很正常。在很长一段时间内,它们比机器更灵活,更像现实世界中的用户。但是,你是否看到每一个传统生产线的生产的成品都有 QA?当然不,那太浪费时间了!当我们制造汽车时,我们不需要检查每一辆车。我们每隔一段时间就会取出一个,以确保流程运行平稳,并相信其余的产品都是一样的。
这也是我们希望在软件开发中实现的操作模式。我们发布了一个版本,甚至一天发布几十个版本。每隔一段时间,就会有人手动验证一个版本,我们相信其他版本也能像这个版本一样工作,因为我们对流程有信心。
但我们目前没有这种信任。有不少公司对部署出现严重错误的历史心有余悸。令人遗憾的是,为在错误发生之前捕获错误,他们引入了一些繁琐的程序。而实际上这些过程会导致相反的结果,并拖累公司的发展。我们有责任重建这种信任,减轻目前手工测试的负担。这可以通过自动化测试流程来实现这一点。
要实现理想的状态,改变被手动测试运行和每天多次发布所阻塞的现状,我们需要投资于自动化测试,以及在组织中建立信任,让我们的代码做它应该做的事情。
现在,我将用一个实例,来自动化一个网站的点击测试。
这里准备一个样例网站,这是一个简单的计数器应用程序。我们将能够点击【Increment】按钮增加一个计数。此外,我们还将提供一个按钮来显示以前没有看到的消息,以便为我们的测试添加更多的动态行为。
下面是这个页面的源码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Testing Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="font-family: sans-serif">
<h1>Testing page</h1>
<!-- Counter interface -->
<button data-test="button-increment" id="js-increment">Increment</button>
<p>Current count: <span data-test="count-output" id="js-count"></span></p>
<!-- Button to show a message -->
<button data-test="button-display" id="js-display">Display Message</button>
<script>
// --- Counter ---
let count = 0
const incrementBtn = document.getElementById('js-increment')
const countSpan = document.getElementById('js-count')
incrementBtn.addEventListener('click', () => {
count += 1
countSpan.innerHTML = count
})
countSpan.innerHTML = count
// --- Display ---
document.getElementById('js-display').addEventListener('click', () => {
const messageContainer = document.createElement('p')
messageContainer.innerHTML = 'A message here'
messageContainer.setAttribute('data-test', 'display')
document.body.appendChild(messageContainer)
})
</script>
</body>
</html>
将这个文件保存在 c:\temp 目录中,以便后继的操作。
如您所见,我们有 HTML 显示计数和一些按钮。然后我们在底部添加了一些 JavaScript 代码来实现逻辑。我们希望该网页:
当我们打开页面时能够正常显示。
当我们点击【Increment】按钮时显示的计数。
按下【Display Message】按钮时显示一个消息对话框。
通常的方式是手动验证它的功能,首先会把网站放在某个服务器上,或者在本地部署一个 Web 服务器上。然后会检查它是否打开并逐一检查需求。现在我们希望能把这些操作用自动化的方式来实现。
我们将使用 Cucumber.js 作为测试框架,Cucumber 是行为驱动测试框架,可以将我们的测试用例与自动化代码结合起来。能够极大的提高团队之间的协作减少沟通的成本。开发选用流行的 Cucumber 开发工具 CukeTest。
Puppeteer 是 Chromium 团队开源的提供能够操作 DevTools 的协议,可以直接操作 Chromium 浏览器。
开发工具使用 CukeTest,完全兼容 Cucumber 的测试工具。
创建新的项目.
在项目目录下安装依赖库:
npm install puppeteer --save
实现功能
在剧本文件 (*.feature) 中定义场景:
# language: zh-CN
功能: 网站测试
主要功能点:
1.计数器功能
2.动态显示
场景: 计数器功能测试
假如打开网页,页面应该有标题显示
并且初始数量应该为0
那么点击Increment按钮时数量应该增加1
场景: 动态显示功能
假如点击Display Message按钮下方动态显示一条数据
定义代码框架:
const { AfterAll, BeforeAll } = require('cucumber');
const puppeteer = require('puppeteer');
let browser = null;
let page = null;
BeforeAll(async function () {
browser = await puppeteer.launch({headless:false,slowMo:200})
page = await browser.newPage();
});
AfterAll(async function () {
await page.close();
await browser.close();
});
上面的 hook 代码分别定义了在项目运行之前和项目运行之后操作,项目运行之前打开浏览器,并新打开一个界面,运行结束后关闭页面和浏览器。
实现操作步骤
在 CukeTest 上可以通过点击可视化界面上每个操作步骤后面的按钮生成代码框架。实现代码后,这个原本灰色的按钮会变成绿色,指示这个代码已实现。
完整代码如下:
const { BeforeAll, AfterAll } = require('cucumber');
const { Given, When, Then } = require('cucumber');
const puppeteer = require('puppeteer');
const assert = require('assert');
let browser = null;
let page = null;
BeforeAll(async function () {
browser = await puppeteer.launch({ headless: false, slowMo: 200 })
page = await browser.newPage();
})
AfterAll(async function () {
await page.close();
await browser.close();
})
Then(/^打开网页,页面应该有标题显示$/, async function () {
await page.goto('file:///C:/temp/index.html')
const pngdata = await page.screenshot({ encoding: 'base64' })
this.attach(pngdata, "image/png");
const headlines = await page.$$('h1')
assert.equal(headlines.length, 1);
})
Given(/^初始数量应该为(\d+)$/, async function (n) {
const count = await page.$eval('[data-test="count-output"]', e => parseInt(e.innerHTML))
const pngdata = await page.screenshot({ encoding: 'base64' })
this.attach(pngdata, "image/png");
assert.equal(count, n)
});
Then(/^点击Increment按钮时数量应该增加(\d+)$/, async function (arg1) {
const incrementBtn = await page.$('[data-test="button-increment"]')
const initialCount = await page.$eval('[data-test="count-output"]', e => parseInt(e.innerHTML))
const expectedCount = initialCount + 1
await incrementBtn.click()
const newCount = await page.$eval('[data-test="count-output"]', e => parseInt(e.innerHTML))
const pngdata = await page.screenshot({ encoding: 'base64' })
this.attach(pngdata, "image/png");
assert.equal(newCount, expectedCount);
});
Given(/^点击Display Message按钮下方动态显示一条数据$/, async function () {
await page.goto("file:///C:/temp/index.html")
await page.$eval('[data-test="button-display"]', e => e.click())
const pngdata = await page.screenshot({ encoding: 'base64' })
this.attach(pngdata, "image/png");
const displays = await page.$$('[data-test="display"]')
assert.equal(displays.length,1);
});
点击 CukeTest 项目运行按钮,即可运行,运行完成后生成报告文件。
可以看到,报告中,在每个操作步骤上添加了截屏图片,方便展示。