跳转到内容

测试

为什么需要测试?

自动化测试可以帮助您和您的团队快速、自信地构建复杂的Vue应用程序,通过防止回归并鼓励您将应用程序分解为可测试的函数、模块、类和组件来实现。与任何应用程序一样,您的新Vue应用程序可能会以多种方式出现故障,因此在发布之前能够捕捉到这些问题并修复它们非常重要。

在本指南中,我们将介绍基本术语,并提供我们对Vue 3应用程序选择哪些工具的建议。

有一个Vue特定的部分涉及可组合性。有关更多详细信息,请参阅下面的测试可组合性

何时进行测试

尽早开始测试!我们建议您一有机会就开始编写测试。您等待添加测试到应用程序的时间越长,应用程序的依赖性就越多,开始测试就越困难。

测试类型

在设计您的Vue应用程序的测试策略时,您应该利用以下测试类型

  • 单元:检查给定函数、类或可组合性的输入是否产生预期的输出或副作用。
  • 组件:检查组件是否挂载、渲染、可交互以及按预期行为。这些测试比单元测试导入的代码更多,更复杂,执行时间更长。
  • 端到端测试:检查跨越多个页面的功能,并对您生产的Vue应用程序进行实际网络请求。这些测试通常需要启动数据库或其他后端。

每种测试类型都在您的应用程序测试策略中扮演着角色,并且每种都能帮助您防止不同类型的问题。

概述

我们将简要讨论这些测试是什么,如何在Vue应用程序中实现它们,并提供一些一般性建议。

单元测试

单元测试的目的是验证小而独立的代码单元是否按预期工作。单元测试通常涵盖一个单独的函数、类、可组合式或模块。单元测试侧重于逻辑正确性,并且只关注应用程序整体功能的一小部分。它们可能会模拟应用程序环境的大部分内容(例如,初始状态、复杂的类、第三方模块和网络请求)。

一般来说,单元测试将捕获函数的业务逻辑和逻辑正确性问题。

以这个 increment 函数为例

js
// helpers.js
export function increment (current, max = 10) {
  if (current < max) {
    return current + 1
  }
  return current
}

因为它非常独立,所以调用 increment 函数并断言它返回它应该返回的内容将非常容易,因此我们将编写一个单元测试。

如果这些断言中的任何一个失败,那么很清楚问题就包含在 increment 函数内部。

js
// helpers.spec.js
import { increment } from './helpers'

describe('increment', () => {
  test('increments the current number by 1', () => {
    expect(increment(0, 10)).toBe(1)
  })

  test('does not increment the current number over the max', () => {
    expect(increment(10, 10)).toBe(10)
  })

  test('has a default max of 10', () => {
    expect(increment(10)).toBe(10)
  })
})

如前所述,单元测试通常应用于独立业务逻辑、组件、类、模块或函数,这些不涉及UI渲染、网络请求或其他环境问题。

这些通常是与Vue无关的纯JavaScript / TypeScript模块。一般来说,在Vue应用程序中编写业务逻辑的单元测试与其他框架使用应用程序的单元测试没有显著差异。

有两个情况您确实需要进行Vue特定功能的单元测试

  1. 可组合式
  2. 组件

可组合式

Vue应用程序中特定的一类函数是可组合式,在测试过程中可能需要特殊处理。有关更多详细信息,请参阅下文的测试可组合式

组件单元测试

组件可以通过两种方式来测试

  1. 白盒:单元测试

    “白盒测试”是了解组件的实现细节和依赖关系的测试。它们专注于隔离正在测试的组件。这些测试通常会模拟组件的一些或所有子组件,以及设置插件状态和依赖关系(例如Pinia)。

  2. 黑盒:组件测试

    “黑盒测试”不了解组件的实现细节。这些测试尽可能模拟最少的内容来测试组件与整个系统的集成。它们通常渲染所有子组件,被视为更“集成测试”。请参阅下文的组件测试建议

建议

  • Vitest

    由于由 create-vue 创建的官方设置基于 Vite,我们建议使用能够直接利用 Vite 的相同配置和转换管道的单元测试框架。专为这一目的设计的单元测试框架 Vitest 由 Vue / Vite 团队成员创建和维护。它与基于 Vite 的项目集成简单,且运行速度极快。

其他选项

  • Jest 是一个流行的单元测试框架。然而,我们只推荐 Jest 如果您有一个现有的 Jest 测试套件需要迁移到基于 Vite 的项目,因为 Vitest 提供了更顺畅的集成和更好的性能。

组件测试

在 Vue 应用中,组件是 UI 的主要构建块。因此,组件是验证应用程序行为时自然隔离的单位。从粒度角度来看,组件测试位于单元测试之上,可以被视为一种集成测试。您的 Vue 应用程序的大部分内容应该由组件测试覆盖,并且我们建议每个 Vue 组件都有自己的 spec 文件。

组件测试应捕获与组件的 props、它提供的 events、slots、styles、classes、生命周期钩子等相关的问题。

组件测试不应模拟子组件,而应通过以用户的方式进行交互来测试组件与其子组件之间的交互。例如,组件测试应像用户一样点击元素,而不是以编程方式与组件交互。

组件测试应专注于组件的公共接口,而不是内部实现细节。对于大多数组件,公共接口仅限于:发出的事件、props 和 slots。在测试时,请记住 测试组件做什么,而不是它是如何做的

DO

  • 对于 视觉 逻辑:根据输入的 props 和 slots 断言正确的渲染输出。

  • 对于 行为 逻辑:断言在用户输入事件响应下正确的渲染更新或发出的事件。

    在下面的示例中,我们展示了一个 Stepper 组件,该组件有一个标记为 "increment" 的 DOM 元素,可以被点击。我们传递一个名为 max 的 prop,该 prop 防止 Stepper 超过 2,所以如果我们点击按钮 3 次,UI 仍然显示 2

    我们对 Stepper 的实现一无所知,只知道 "input" 是 max prop,而 "output" 是用户将看到的 DOM 状态。

Vue Test Utils
Cypress
Testing Library
js
const valueSelector = '[data-testid=stepper-value]'
const buttonSelector = '[data-testid=increment]'

const wrapper = mount(Stepper, {
  props: {
    max: 1
  }
})

expect(wrapper.find(valueSelector).text()).toContain('0')

await wrapper.find(buttonSelector).trigger('click')

expect(wrapper.find(valueSelector).text()).toContain('1')

DON'T

  • 不要断言组件实例的私有状态或测试组件的私有方法。测试实现细节会使测试变得脆弱,因为它们更有可能在实现更改时崩溃并需要更新。

    组件的最终任务是渲染正确的 DOM 输出,因此专注于 DOM 输出的测试提供了相同的正确性保证(如果不是更多),同时更稳健且对变化更具弹性。

    不要完全依赖快照测试。断言 HTML 字符串并不能描述正确性。编写有目的性的测试。

    如果一个方法需要彻底测试,考虑将其提取为一个独立的实用函数,并为它编写专门的单元测试。如果无法干净地提取,它可以作为组件、集成或端到端测试的一部分进行测试,该测试涵盖了它。

建议

Vitest与基于浏览器的运行程序之间的主要区别是速度和执行上下文。简而言之,基于浏览器的运行程序,如Cypress,可以捕捉到基于节点的运行程序(如Vitest)无法捕获的问题(例如,样式问题、真实的原生DOM事件、cookie、本地存储和网络故障),但基于浏览器的运行程序比Vitest慢得多,因为它们确实打开了浏览器、编译了您的样式表等等。Cypress是一个支持组件测试的基于浏览器的运行程序。请阅读 Vitest的比较页面 了解关于Vitest和Cypress的最新信息。

挂载库

组件测试通常涉及将正在测试的组件单独挂载,触发模拟的用户输入事件,并断言渲染的DOM输出。有一些专门的实用库可以简化这些任务。

  • @vue/test-utils 是官方的低级组件测试库,它被编写来为用户提供访问Vue特定API的权限。它也是 @testing-library/vue 所构建在之上的低级库。

  • @testing-library/vue 是一个Vue测试库,专注于在不依赖实现细节的情况下测试组件。其指导原则是,测试越接近软件的使用方式,它们可以提供的信心就越大。

我们建议在应用程序中测试组件时使用 @vue/test-utils。由于 @testing-library/vue 在测试使用Suspense的异步组件时存在问题,因此应谨慎使用。

其他选项

  • Nightwatch 是一个具有Vue组件测试支持的端到端测试运行程序。(示例项目

  • WebdriverIO 用于基于标准化自动化的跨浏览器组件测试,它依赖于原生用户交互。它也可以与Testing Library一起使用。

端到端测试

虽然单元测试可以为开发者提供一定程度的信心,但当部署到生产环境时,单元和组件测试在提供对应用程序的整体覆盖方面的能力有限。因此,端到端(E2E)测试提供了对应用程序最关键方面的覆盖:当用户实际使用您的应用程序时会发生什么。

端到端测试关注于多页应用行为,这些行为会针对您生产构建的Vue应用程序发起网络请求。它们通常涉及启动数据库或其他后端,甚至可以在实时预发布环境中运行。

端到端测试通常会捕获与您的路由、状态管理库、顶级组件(例如App或Layout)、公共资产或任何请求处理相关的错误。如上所述,它们可以捕获单元测试或组件测试难以发现的关键错误。

端到端测试不导入Vue应用程序的任何代码,而是完全通过在真实浏览器中导航整个页面来测试您的应用程序。

端到端测试验证了您应用程序中的多个层级。它们可以针对您本地构建的应用程序,甚至是一个实时预发布环境。针对预发布环境的测试不仅包括您的前端代码和静态服务器,还包括所有相关的后端服务和基础设施。

您的测试越接近您软件的使用方式,它们就能给您带来越多的信心。 —— Kent C. Dodds —— Testing Library的作者

通过测试用户操作如何影响您的应用程序,端到端测试通常是判断应用程序是否正常工作的重要关键。

选择端到端测试解决方案

尽管在网络上进行的端到端(E2E)测试因其不可靠(易出故障)的测试和减缓开发流程而获得了负面声誉,但现代E2E工具已经取得了长足的进步,以创建更可靠、互动和有用的测试。在选择E2E测试框架时,以下部分提供了一些在选择适用于您应用程序的测试框架时应考虑的指导。

跨浏览器测试

端到端(E2E)测试广为人知的主要好处之一是能够跨多个浏览器测试您的应用程序。虽然拥有100%的跨浏览器覆盖率可能看起来很有吸引力,但需要注意的是,由于运行它们所需的额外时间和机器功率,跨浏览器测试在团队资源上的回报是递减的。因此,在确定您的应用程序需要的跨浏览器测试量时,应谨慎考虑这种权衡。

更快的反馈循环

端到端(E2E)测试和开发的一个主要问题是运行整个套件需要很长时间。通常,这仅在持续集成和持续部署(CI/CD)管道中完成。现代E2E测试框架通过添加诸如并行化等功能来帮助解决这个问题,这使得CI/CD管道往往比以前快得多。此外,在本地开发时,能够选择性地运行您正在工作的页面的单个测试,同时提供测试的热重载,可以帮助提高开发者的工作流程和生产力。

一流的调试体验

虽然开发者传统上依赖于在终端窗口中扫描日志来确定测试中发生了什么错误,但现代端到端(E2E)测试框架允许开发者利用他们已经熟悉的工具,例如浏览器开发者工具。

无头模式下的可见性

当在持续集成/部署管道中运行端到端(E2E)测试时,它们通常在无头浏览器中运行(即,不会为用户打开可见的浏览器)。现代E2E测试框架的关键特性是能够在测试期间查看应用程序的快照和/或视频,从而为错误发生的原因提供一些洞察。历史上,维护这些集成相当繁琐。

建议

  • Playwright 是一款优秀的E2E测试解决方案,支持Chromium、WebKit和Firefox。在Windows、Linux和macOS上本地或CI上进行测试,无头或带头,具有谷歌Chrome的Android原生移动模拟和Mobile Safari。它具有信息丰富的UI、出色的调试能力、内置断言、并行化、跟踪,并旨在消除易变测试。支持 组件测试,但标记为实验性。Playwright是开源的,由微软维护。

  • Cypress 具有信息丰富的图形界面、出色的调试能力、内置断言、存根、抗抖动和快照。如前所述,它提供稳定的 组件测试 支持。Cypress支持基于Chromium的浏览器、Firefox和Electron。WebKit支持可用,但标记为实验性。Cypress采用MIT许可证,但某些功能(如并行化)需要订阅Cypress Cloud。

其他选项

  • Nightwatch 是一种基于 Selenium WebDriver 的E2E测试解决方案。这使它拥有最广泛的浏览器支持范围,包括原生移动测试。基于Selenium的解决方案将比Playwright或Cypress慢。

  • WebdriverIO 是基于WebDriver协议的Web和移动测试自动化框架。

食谱

将Vitest添加到项目中

在一个基于Vite的Vue项目中,运行

sh
> npm install -D vitest happy-dom @testing-library/vue

接下来,更新Vite配置以添加test选项块

js
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  // ...
  test: {
    // enable jest-like global test APIs
    globals: true,
    // simulate DOM with happy-dom
    // (requires installing happy-dom as a peer dependency)
    environment: 'happy-dom'
  }
})

提示

如果您使用TypeScript,请将vitest/globals添加到tsconfig.json中的types字段。

json
// tsconfig.json

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

然后,在项目根目录中创建一个以*.test.js结尾的文件。您可以将所有测试文件放置在项目根目录中的测试目录中或放在源文件旁边的测试目录中。Vitest将自动使用命名约定搜索它们。

js
// MyComponent.test.js
import { render } from '@testing-library/vue'
import MyComponent from './MyComponent.vue'

test('it should work', () => {
  const { getByText } = render(MyComponent, {
    props: {
      /* ... */
    }
  })

  // assert output
  getByText('...')
})

最后,更新package.json以添加测试脚本并运行它

json
{
  // ...
  "scripts": {
    "test": "vitest"
  }
}
sh
> npm test

测试组合式

本节假定您已阅读组合式部分。

当涉及到测试组合式时,我们可以将它们分为两类:不依赖于宿主组件实例的组合式,以及依赖于宿主组件实例的组合式。

当组合式使用以下API时,它依赖于宿主组件实例:

  • 生命周期钩子
  • 提供 / 注入

如果组合式仅使用响应性API,则可以直接调用它并断言其返回的状态/方法进行测试

js
// counter.js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++

  return {
    count,
    increment
  }
}
js
// counter.test.js
import { useCounter } from './counter.js'

test('useCounter', () => {
  const { count, increment } = useCounter()
  expect(count.value).toBe(0)

  increment()
  expect(count.value).toBe(1)
})

依赖于生命周期钩子或提供/注入的组合式需要包装在宿主组件中进行测试。我们可以创建以下类型的辅助器

js
// test-utils.js
import { createApp } from 'vue'

export function withSetup(composable) {
  let result
  const app = createApp({
    setup() {
      result = composable()
      // suppress missing template warning
      return () => {}
    }
  })
  app.mount(document.createElement('div'))
  // return the result and the app instance
  // for testing provide/unmount
  return [result, app]
}
js
import { withSetup } from './test-utils'
import { useFoo } from './foo'

test('useFoo', () => {
  const [result, app] = withSetup(() => useFoo(123))
  // mock provide for testing injections
  app.provide(...)
  // run assertions
  expect(result.foo.value).toBe(1)
  // trigger onUnmounted hook if needed
  app.unmount()
})

对于更复杂的组合式,也可以通过使用组件测试技术针对包装组件编写测试来更容易地进行测试。

测试已加载