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
为例
代码如下:
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&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