阅读 161

Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)

在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十)

我们学到了通过conventional-changelog 来生成项目的Changelog更新日志,通过更新日志我们可以更好记录版本的更新信息,以及每一个更新所对应的代码有哪些变更。这样项目的可维护性会变得更好。

今天我们要看的是E2E测试,也是Vuex源码中所集成的,和单元测试有所区别,单元测试主要测试每个函数,一个小的单元。而E2E测试主要是测试整个端到端,也就是实际的前端展示是否是正确的,符合预期。

我们通过Vuex的源码真的可以学到很多的东西,虽然项目的代码不是很多,才1000多行,但项目的各方面质量保证很好,NPM上每周下载量为159万,通过学习优秀的开源项目,我们把这些点都应用到自己的项目里。

老规矩,还是从package.json看起,我们可以找到一个test:e2e的命令

"scripts": {   //...   "dev": "node examples/server.js",   "test:e2e": "start-server-and-test dev http://localhost:8080 \"jest --testPathIgnorePatterns test/unit\"",   //... } 复制代码

要运行E2E测试,由于是要测试项目真实的展示情况,所以需要跑起项目,这里Vuex用到start-server-and-test这个依赖工具,这里不禁的感叹Node.js的NPM生态真是太强大了,什么工具都有,不知道还有什么其他语言的生态能做到吗?除了Java。

这个start-server-and-test所做的事情就是,启动服务器,等待URL加载,然后运行测试命令,当测试结束时,再关闭服务器。

上面命令的前面部分start-server-and-test dev,就相当于运行了npm run dev命令来启动webpack开发服务器,然后接下来等待http://localhost:8080加载,加载完成后执行jest --testPathIgnorePatterns test/unit这个jest测试命令来使用jest来运行我们的整个E2E测试。

我们可以看到e2e下面有4个测试文件,以cart.spec.js为例

image-20211129205146803

代码如下:

import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers' describe('e2e/cart', () => {   const { page, text, count, click, sleep } = setupPuppeteer()   async function testCart (url) {     await page().goto(url)     await sleep(120) // api simulation     expect(await count('li')).toBe(3)     expect(await count('.cart button[disabled]')).toBe(1)     expect(await text('li:nth-child(1)')).toContain('iPad 4 Mini')     expect(await text('.cart')).toContain('Please add some products to cart')     expect(await text('.cart')).toContain('Total: $0.00')     await click('li:nth-child(1) button')     expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 1')     expect(await text('.cart')).toContain('Total: $500.01')     await click('li:nth-child(1) button')     expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 2')     expect(await text('.cart')).toContain('Total: $1,000.02')     expect(await count('li:nth-child(1) button[disabled]')).toBe(1)     await click('li:nth-child(2) button')     expect(await text('.cart')).toContain('H&M T-Shirt White - $10.99 x 1')     expect(await text('.cart')).toContain('Total: $1,011.01')     await click('.cart button')     await sleep(200)     expect(await text('.cart')).toContain('Please add some products to cart')     expect(await text('.cart')).toContain('Total: $0.00')     expect(await text('.cart')).toContain('Checkout successful')     expect(await count('.cart button[disabled]')).toBe(1)   }   test('classic', async () => {     await testCart('http://localhost:8080/classic/shopping-cart/')   }, E2E_TIMEOUT)   test('composition', async () => {     await testCart('http://localhost:8080/composition/shopping-cart/')   }, E2E_TIMEOUT) }) 复制代码

在代码顶部,引入了test/helpers.js中的工具函数setupPuppeteer和常量E2E_TIMEOUT

import puppeteer from 'puppeteer' // 每个测试的超时时间 export const E2E_TIMEOUT = 30 * 1000 // puppeteer启动参数 const puppeteerOptions = process.env.CI   ? { args: ['--no-sandbox', '--disable-setuid-sandbox'] }   : {} export function setupPuppeteer () {   let browser   let page   // 运行每条测试前要执行的函数,可以理解为jest的生命周期   beforeEach(async () => {     browser = await puppeteer.launch(puppeteerOptions)     page = await browser.newPage()     page.on('console', (e) => {       if (e.type() === 'error') {         const err = e.args()[0]         console.error(           `Error from Puppeteer-loaded page:\n`,           err._remoteObject.description         )       }     })   })   // 同理,运行每条测试后要执行的函数   afterEach(async () => {     await browser.close()   })   // 点击元素   async function click (selector, options) {     await page.click(selector, options)   }   // 经过元素   async function hover (selector) {     await page.hover(selector)   }   // 按键抬起   async function keyUp (key) {     await page.keyboard.up(key)   }   // 统计元素数量   async function count (selector) {     return (await page.$$(selector)).length   }   // 返回元素的文本   async function text (selector) {     return await page.$eval(selector, (node) => node.textContent)   }   // 返回元素的value   async function value (selector) {     return await page.$eval(selector, (node) => node.value)   }   // 返回元素的html   async function html (selector) {     return await page.$eval(selector, (node) => node.innerHTML)   }   // 返回元素的所有class   async function classList (selector) {     return await page.$eval(selector, (node) => {       const list = []       for (const index in node.classList) {         list.push(node.classList[index])       }       return list     })   }   // 判断元素是否有某个class   async function hasClass (selector, name) {     return (await classList(selector)).find(c => c === name) !== undefined   }   // 是否是隐藏状态   async function isVisible (selector) {     const display = await page.$eval(selector, (node) => {       return window.getComputedStyle(node).display     })     return display !== 'none'   }   // 是否为选中状态   async function isChecked (selector) {     return await page.$eval(selector, (node) => node.checked)   }   // 是否是焦点状态   async function isFocused (selector) {     return await page.$eval(selector, (node) => node === document.activeElement)   }   // 设置value   async function setValue (selector, value) {     const el = (await page.$(selector))     await el.evaluate((node) => { node.value = '' })     await el.type(value)   }   // 设置value,并按下回撤   async function enterValue (selector, value) {     const el = (await page.$(selector))     await el.evaluate((node) => { node.value = '' })     await el.type(value)     await el.press('Enter')   }   // 清空value   async function clearValue (selector) {     return await page.$eval(selector, (node) => { node.value = '' })   }   // 等待多少毫秒   async function sleep (ms = 0) {     return new Promise((resolve) => {       setTimeout(resolve, ms)     })   }   // 返回这些工具函数   return {     page: () => page,     click,     hover,     keyUp,     count,     text,     value,     html,     classList,     hasClass,     isVisible,     isChecked,     isFocused,     setValue,     enterValue,     clearValue,     sleep   } } 复制代码

从依赖我们可以看到,实际我们是使用puppeteer这个库来实现访问页面,像浏览器一样去操作页面。

puppeteer是由Google开源的一个 Node 库,它提供了各种高级 API 来通过 DevTools 协议控制 Chrome 或 Chromium。 Puppeteer 默认无头运行,但可以配置为运行完整(非无头)Chrome 或 Chromium。

现在大多数E2E测试都会使用到puppeteer

现在,我们结合页面的HTML在回来看这些测试用例就很好理解了

访问:http://localhost:8080/classic/shopping-cart/,可以看到HTML结构

<div id="app">    <h1> Shopping Cart Example </h1>    <hr />    <h2> Products </h2>    <ul>      <li> iPad 4 Mini-$500.01 <br /> <button> Add to cart </button> </li>      <li> H&amp;M T-Shirt White-$10.99 <br /> <button> Add to cart </button> </li>      <li> Charli XCX-Sucker CD-$19.99 <br /> <button> Add to cart </button> </li>    </ul>    <hr />    <div class="cart">      <h2> Your Cart </h2>      <p> <i> Please add some products to cart. </i> </p>      <ul>      </ul>      <p> Total:$0.00 </p>      <p> <button disabled=""> Checkout </button> </p>      <p style="display: none;"> Checkout. </p>    </div>  </div> 复制代码

async function testCart (url) {   // 进入http://localhost:8080/classic/shopping-cart/页面   await page().goto(url)   // 等待120毫秒   await sleep(120) // api simulation   // li元素是否为3个   expect(await count('li')).toBe(3)   // 禁用的Checkout元素的数量   expect(await count('.cart button[disabled]')).toBe(1)   // 第一个li元素包含的内容是否有:iPad 4 Mini   expect(await text('li:nth-child(1)')).toContain('iPad 4 Mini')   // cart元素是否包含文字:Please add some products to cart   expect(await text('.cart')).toContain('Please add some products to cart')   // cart元素是否包含文字:Total: $0.00   expect(await text('.cart')).toContain('Total: $0.00')   // 点击第一个元素的 Add to cart按钮   await click('li:nth-child(1) button')   // cart元素是否包含文字:iPad 4 Mini - $500.01 x 1   expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 1')   // cart元素是否包含文字:Total: $500.01   expect(await text('.cart')).toContain('Total: $500.01')   // 点击第一个元素的 Add to cart按钮   await click('li:nth-child(1) button')   // cart元素是否包含文字:iPad 4 Mini - $500.01 x 2   expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 2')   // cart元素是否包含文字:Total: $1,000.02   expect(await text('.cart')).toContain('Total: $1,000.02')   // 第一个元素的 Add to cart按钮 是否为禁用状态   expect(await count('li:nth-child(1) button[disabled]')).toBe(1)   // 点击第二个元素的 Add to cart按钮   await click('li:nth-child(2) button')   // cart元素是否包含文字:H&M T-Shirt White - $10.99 x 1   expect(await text('.cart')).toContain('H&M T-Shirt White - $10.99 x 1')   // cart元素是否包含文字:Total: $1,011.01   expect(await text('.cart')).toContain('Total: $1,011.01')   // 点击Checkout按钮   await click('.cart button')   // 等待200毫秒   await sleep(200)   // cart元素是否包含文字:Please add some products to cart   expect(await text('.cart')).toContain('Please add some products to cart')   // cart元素是否包含文字:Total: $0.00   expect(await text('.cart')).toContain('Total: $0.00')   // cart元素是否包含文字:Checkout successful   expect(await text('.cart')).toContain('Checkout successful')   // Checkout按钮是否是禁用状态   expect(await count('.cart button[disabled]')).toBe(1) } 复制代码

这是cart.spec.js这一个测试,其他的测试也是同理,我们可以通过修改测试代码来查看测试结果

比如修改最后一行:

expect(await count('.cart button[disabled]')).toBe(2) 复制代码

可以看到测试并没有通过,并且展示出是哪条测试没有通过

Test Suites: 0 of 4 total   ● e2e/cart › classic     expect(received).toBe(expected) // Object.is equality     Expected: 2     Received: 1       33 |     expect(await text('.cart')).toContain('Total: $0.00')       34 |     expect(await text('.cart')).toContain('Checkout successful')     > 35 |     expect(await count('.cart button[disabled]')).toBe(2)          |                                                   ^       36 |   }       37 |        38 |   test('classic', async () => { 复制代码

我们可以修改后,再次执行测试,全部通过。

 PASS  test/e2e/cart.spec.js  PASS  test/e2e/todomvc.spec.js  PASS  test/e2e/counter.spec.js  PASS  test/e2e/chat.spec.js (5.219 s) Test Suites: 4 passed, 4 total Tests:       8 passed, 8 total Snapshots:   0 total Time:        6.106 s, estimated 7 s Ran all test suites.


作者:小帅的编程笔记
链接:https://juejin.cn/post/7035990484417773605


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐