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.

感觉大家的阅读

一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值