之前我们使用Puppeteer进行网页爬虫(以及自动化操作),这篇文章主要验证一下Puppeteer测试的可实现性。
项目设置
让我们从设置一个基本的React应用程序开始。 我们将安装其他依赖项,如Puppeteer和Faker。
为了这篇文章的目的,我创建了一个简单的应用程序,其中包含一个表单,并在表单提交时呈现成功消息。 将此应用程序克隆到您的系统中。
git clone https://github.com/rajatgeekyants/test.git
现在,让我们安装开发依赖项。
yarn install
我们不需要安装Jest,它已经在React程序包中预装好了。 如果你再次尝试安装它,你的测试将不起作用,因为两个Jest版本将相互冲突。
接下来,我们需要在 package.json
中更新 test
脚本来调用 Jest。 我们还将添加另一个名为 debug
的脚本。 此脚本将我们的 Node 环境变量设置为调试模式并调用 npm test
。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "jest",
"debug": "NODE_ENV=debug npm test",
"eject": "react-scripts eject",
}
使用 Puppeteer,我们可以在无头模式或 Chromium 浏览器中运行我们的测试。 这是一个很好的功能,因为它允许我们查看测试正在评估的视图、DevTools 和网络请求。 唯一的缺点是它会使 Continuous Integration (CI) 的速度非常慢。
我们可以使用环境变量来决定是否以无头模式运行我们的测试。 我将以这样的方式设置我的测试,即当我想看到它们被评估时,我可以运行 debug
脚本。 当我不想看到时,我会运行 test
脚本。
转到 src/App
并创建一个名为 App.test.js
的新文件。 在其中编写以下代码。
我们首先告诉我们的应用程序我们 require
Puppeteer。 然后,我们 describe
我们的第一个测试,其中我们检查初始页面加载。 在这里,我正在测试 h1
标签是否包含正确的文本。
在我们的测试描述中,我们需要定义 browser
和 page
变量。 这些是遍历测试所必需的。
launch
方法帮助我们通过浏览器配置选项,并使我们能够在不同的浏览器设置中控制和测试我们的应用程序。 我们甚至可以通过设置仿真选项来更改浏览器页面的设置。
让我们首先设置我们的浏览器。 我在文件顶部创建了一个名为 isDebugging
的函数。 我们将在 launch 方法中调用此函数。 此函数将具有名为 debugging_mode
的对象,其中包含三个属性:
headless: false
—— 是否以无头模式(true
)或在 Chromium 浏览器中(false
)运行我们的测试slowMo: 250
—— 通过 250 毫秒减慢 Puppeteer 操作的速度。devtools: true
—— 浏览器与应用程序交互时是否应打开 DevTools (true
)。
然后,isDebugging
函数将返回一个三元表达式,该表达式基于环境变量。 三元表达式决定应用程序是否应返回 debuggin_mode
对象还是空对象。
回到我们的 package.json
文件中,我们创建了一个 debug
脚本,该脚本将我们的 Node 环境变量设置为调试。 与我们的测试不同,isDebugging
函数将返回我们的自定义浏览器选项,这取决于我们的环境变量 debug
。
接下来,我们正在为页面设置一些选项。 这是在 page.emulate
方法中完成的。 我们正在设置 viewport
属性的 width
和 height
,并将 userAgent
设置为空字符串。
page.emulate
非常有用,因为它使我们能够在各种浏览器选项下运行测试。 我们还可以使用 page.emulate
复制不同的页面属性。
使用 Puppeteer 测试 HTML 内容
我们现在准备开始为我们的 React 应用编写测试。 在本节中,我将测试 <h1>
标签和导航,并确保它们正常工作。
打开 App.test.js
文件,在 test
块中 page.emulate
声明的正下方编写以下代码:
基本上,我们正在告诉 Puppeteer 转到 url http://localhost:3000/
. 。 Puppeteer 将评估 App-title
类。 这个类出现在我们的 h1
标签上。
$.eval
方法实际上是在它传入的任何框架内运行 document.querySelector
。
Puppeteer 找到匹配此类的选择器,它将传递该选择器给回调函数 e.innerHTML
。 在这里,Puppeteer 将能够提取 <h1>
元素,并检查它是否说 Welcome to React
。
一旦 Puppeteer 完成测试,browser.close
将关闭浏览器。
打开命令终端并运行 debug
脚本。
yarn debug
如果你的应用程序通过了测试,你应该在控制台中看到类似的内容:
接下来,转到 src/App/index.js
,你会看到这样的 nav
元素:
<nav className='navbar'>
<ul>
<li className="nav-li"><a href="#">Batman</a></li>
<li className="nav-li"><a href="#">Supermman</a></li>
<li className="nav-li"><a href="#">Aquaman</a></li>
<li className="nav-li"><a href="#">Wonder Woman</a></li>
</ul>
请注意,所有 <li>
元素都具有相同的类。 返回到 App.test.js
并编写导航测试。
在此之前,让我们重构我们之前编写的代码
let browser
let page
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging())
page = await browser.newPage()
await page.goto('http://localhost:3000/')
page.setViewport({ width: 500, height: 2400 })
})
之前,我在userAgent
上没有任何内容。 所以,我只是使用setViewport
而不是beforeAll
。 现在,我可以摆脱 localhost
和 browser.close
,并使用 afterAll
。 如果应用程序处于调试模式,那么我希望删除该浏览器。
afterAll(() => {
if (isDebugging()) {
browser.close()
}
})
我们现在可以继续编写导航测试了。 在 describe
块中,创建一个新的 test
,如下所示。
test('nav loads correctly', async () => {
const navbar = await page.$eval('.navbar', el => el ? true : false)
const listItems = await page.$$('.nav-li')
expect(navbar).toBe(true)
expect(listItems.length).toBe(4)
});
这里,我首先使用 .navbar
类上的 $eval
函数获取 navbar
。 然后我使用三元运算符返回 true
或 false
来查看元素是否存在。
接下来,我需要获取列表项。 就像之前一样,我在 nav-li
类上使用 $eval
函数。 我们将 expect
navbar
为 true
,listItems
的长度等于 4。
您可能已经注意到我在 listItems
上使用了 $$
。 这是从页面内运行 document.querySelectorAll
的快捷方式。 当没有与美元符号一起使用 eval
时,就不会有回调。
运行调试脚本以查看代码是否可以通过这两个测试。
模拟用户活动
让我们看看如何通过模拟键盘输入、鼠标单击和触屏事件来测试表单提交。 这将使用 Faker 生成的随机用户信息来完成。
在 src
文件夹中,我创建了一个登录组件 Login
。 这只是一个带有四个输入框和一个提交按钮的表单。
这里是用 Bit 共享的组件,以便您可以使用 NPM 安装它或直接从自己的项目中导入和开发它。
Bit - 登录/src/app - 来自 geekrajat 的 React 组件
登录成功后显示成功消息的登录组件 - 用 react 写的。 依赖项:react。登录表单的作用域…
bitsrc.io
当用户单击“登录”按钮时,应用程序需要显示成功消息。 这是我创建的另一个组件。
我已经向类 App
添加了一个 state
,以及一个 handleSubmit
方法,该方法将阻止默认函数并将 complete
的值更改为 true
。
state = { complete: false }
handleSubmit = e => {
e.preventDefault()
this.setState({ complete: true })
}
页面底部还有一个三元语句。 这将决定是显示 Login
还是 SuccessMessage
。
{ this.state.complete ?
<SuccessMessage/>
:
<Login submit={this.handleSubmit} />
}
运行 yarn start
确保您的应用程序完美运行。
我现在将使用 Puppeteer 编写端到端测试,以确保此功能正常工作。 转到 App.test.js
文件并导入 faker
。 然后我将像这样创建一个 user
对象:
const faker = require('faker')
const user = {
email: faker.internet.email(),
password: 'test',
firstName: faker.name.firstName(),
lastName: faker.name.lastName()
}
在测试中,Faker 非常有用,因为每次运行测试时,它都会生成不同的数据。
在 describe
块中编写一个新的 test
来测试登录表单。 测试将点击我们的属性并向其中输入一些内容。 然后测试将 click
提交按钮并等待成功消息。 我还会为这个 test
添加超时。
test('login form works correctly', async () => {
// 点击firstName输入框
await page.click('[data-testid="firstName"]')
// 在lastName输入框中输入firstName
await page.type('[data-testid="lastName"]', user.firstName)
// 其他输入框的操作
// 点击提交按钮
await page.click('[data.testid="submit"]')
// 等待成功信息显示
await page.waitForSelector('[data-testid="success"]')
}, 1600)
运行 debug
脚本并观察 Puppeteer 如何进行测试!
在测试中设置 Cookie
我现在希望每次提交表单时,应用程序都会将 cookie 保存到页面上。 此 cookie 将保存用户的名字。
为了简单起见,我将重构我的 App.test.js
文件以仅打开一个页面。 这个页面将模拟 iPhone 6。
我想在表单提交时保存 cookie,我们将在表单的上下文中添加测试。
为登录表单编写一个新的 describe
块,然后将我们的登录表单测试复制并粘贴到其中。
describe('login form', () => {
// 在其中插入登录表单
})
我还将测试重命名为 fills out form and submits
。 现在创建一个名为 sets firstName cookie
的新测试块。 此测试将检查是否设置了 firstNameCookie
。
test('sets firstName cookie', async () => {
const cookies = await Page.cookies()
const firstNameCookie = cookies.find(c => c.name === 'firstName' && c.value === user.firstName)
expect(firstNameCookie).not.toBeUndefined()
})
Page.cookies
将返回表示每个文档 cookie 的对象数组。 我使用数组原型方法 find
来查看 cookie 是否存在。 好的,我继续翻译:
这将确保应用程序正在使用 Faker 生成的 firstName
。
如果您现在在终端中运行 test
脚本,您会看到测试失败,因为它返回给我们一个未定义的值。 让我们现在解决这个问题。
在 App.js
文件中,向 state
对象添加一个 firstName
属性。 它将是一个空字符串。
state = {
complete: false,
firstName: '',
}
在 handleSubmit
方法中添加:
document.cookie = `firstName=${this.state.firstname}`
创建一个名为 handleInput
的新方法。 每个输入时都会触发此方法以更新状态。
handleInput = e => {
this.setState({firstName: e.currentTarget.value})
}
将此方法作为 prop 传递给 Login
组件。
<Login submit={this.handleSubmit} input={this.handleInput} />
在 Login.js
文件中,向 firstName
输入添加 onChange={props.input}
。 通过这种方式,每当用户在 firstName
输入中键入内容时,React 就会触发此输入方法。
现在我需要应用程序在用户单击“登录”按钮时将 firstName
cookie 保存到页面上。 运行 npm test
以查看您的应用程序是否通过了所有测试。
如果应用程序需要在执行任何操作之前存在某个 cookie,并且此 cookie 是在一系列之前授权的页面上设置的怎么办?
在 App.js
文件中,按如下方式重构 handleSubmit
方法:
handleSubmit = e => {
e.preventDefault()
if (document.cookie.includes('JWT')){
this.setState({ complete: true })
}
document.cookie = `firstName=${this.state.firstName}`
}
使用这段代码,只有当文档包含 JWT
时,SuccessMessage
组件才会加载。
在 App.test.js
文件中进入 fills out form and submits
测试块并编写以下内容:
await page.setCookie({ name: 'JWT', value: 'kdkdkddf' })
这将设置一个实际设置 JSON Web 令牌'JWT'
的 cookie,里面是一些随机的测试内容。 如果您现在在终端中运行 test
脚本,您的应用程序将运行所有测试并通过!
使用 Puppeteer 截图
截图可以帮助我们查看测试失败时它正在查看的内容。 让我们看看如何使用 Puppeteer 拍摄截图并分析我们的测试。
在我们的 App.test.js
文件中,执行名为 nav loads correctly
的 test
。 添加一个条件语句,检查 listItems
的长度不等于 3。 如果是这种情况,那么 Puppeteer 应该截取页面的屏幕截图,并更新测试以期望 listItems
的长度为 3 而不是 4。
if (listItems.length !== 3)
await page.screenshot({path: 'screenshot.png'});
expect(listItems.length).toBe(3);
我们的测试明显会失败,因为在我们的应用程序中我们有 4 个 listItems
。 在终端中运行 test
脚本并观察测试失败。 同时,您会在应用程序的根目录中找到一个名为 screenshot.png
的新文件。
您还可以配置截图方法:
fullPage
—— 如果为true
,Puppeteer 将截取整个页面的屏幕截图。quality
—— 这的范围是从 0 到 100,并设置图像的质量。clip
—— 这需要一个对象,该对象指定要截图的页面的剪裁区域。
您还可以通过执行 page.pdf
而不是 page.screenshot
来创建页面的 PDF。 这具有其自己的唯一配置。
scale
—— 这是一个数字,指网页渲染。 默认值为 1。format
—— 这指的是纸张格式。 如果设置了它,它将优先于传递给它的任何宽度或高度选项。 默认值为letter
margin
—— 这是指页边距。
在测试中处理页面请求
让我们看看 Puppeteer 如何在测试中处理页面请求。 在 App.js
文件中,我将编写一个异步的 componentDidMount
方法。 此方法将从 Pokemon API 获取数据。 对此获取请求的响应将以 JSON 文件的形式返回。 我也会将这些数据添加到我的状态中。
async componentDidMount() {
const data = await fetch('https://pokeapi.co/api/v2/pokedex/1/').then(res => res.json())
this.setState({pokemon: data})
}
请确保向状态对象添加 pokemon: {}
。 在应用组件中,添加此 <h3>
标签。
<h3 data-testid="pokemon">
{this.state.pokemon.next ? 'Received Pokemon data!' : 'Something went wrong'}
</h3>
如果运行您的应用程序,您将看到应用程序已成功获取数据。
使用 Puppeteer,我可以编写任务来检查 <h3/>
标签的内容在成功请求的情况下,以及拦截请求并强制失败的情况。 通过这种方式,我可以查看应用程序在成功和失败的情况下的运行方式。
我首先要让 Puppeteer 发送一个请求来拦截获取请求。 然后,如果我的 url 包含“pokeapi”这个词,那么 Puppeteer 应该中止拦截的请求。 否则,一切照旧。
打开 App.test.js
文件并在 beforeAll
方法中编写以下代码。
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url.includes('pokeapi')) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}
});
setRequestInterception
是一个标志,它允许我访问页面发出的每个请求。 一旦请求被拦截,就可以使用特定的错误代码中止请求。 我可以强制失败,也可以只是拦截请求并在检查某些条件逻辑后继续。
让我们编写一个名为 fails to fetch pokemon
的新 test
。 此测试将评估 h3
标签。 然后我将获取内部 HTML 并确保此文本中的内容为 Received Pokemon data!
。
test('fails to fetch pokemon', async () => {
// 检查 h3 标签的内容
expect(h3Content).toContain('Received Pokemon data!');
});
运行 debug
脚本,这样您就可以实际看到 <h3/>
。 您会注意到整个时间 Something went wrong
文本保持不变。 我们的所有测试都通过了,这意味着我们成功中止了 Pokemon 请求。
请注意,在拦截请求时,我们可以控制发送的标头是什么,返回什么错误代码以及返回自定义主体响应。
最后
这个就是简单的测试用例,实际中我们可以不需要jtest,我们只需要 “===” 也是可以校验。嘿嘿