在上一篇笔记中: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.
感觉大家的阅读
一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新