Puppeteer结合Jest对网页进行测试

之前我们使用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 标签是否包含正确的文本。

在我们的测试描述中,我们需要定义 browserpage 变量。 这些是遍历测试所必需的。

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 属性的 widthheight,并将 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。 现在,我可以摆脱 localhostbrowser.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。 然后我使用三元运算符返回 truefalse 来查看元素是否存在。

接下来,我需要获取列表项。 就像之前一样,我在 nav-li 类上使用 $eval 函数。 我们将 expect navbartrue,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 correctlytest。 添加一个条件语句,检查 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,我们只需要 “===” 也是可以校验。嘿嘿

  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也想MK代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值