Back

在 Vue 测试中使用 Vitest 模拟 API 调用

在 Vue 测试中使用 Vitest 模拟 API 调用

Vue 组件很少单独存在。它们大多数会获取数据、提交表单或轮询端点。当你在不控制网络的情况下测试这些组件时,你的测试会变得缓慢、不稳定,并依赖于外部服务。在 Vue 测试中模拟 API 调用有助于消除这些问题,让你能够控制组件接收的响应。

本文介绍了两种实用的 Vitest Vue API 模拟策略:使用 vi.mock 直接替换 API 模块,以及使用 Mock Service Worker (MSW) 在 HTTP 层拦截请求。你还将学习使这两种方法可靠工作所需的 Vue 特定异步处理。

核心要点

  • 测试中的真实网络调用会产生不稳定、缓慢、不确定的结果——模拟让你完全控制响应。
  • vi.mock 在导入级别替换 API 模块,非常适合快速、隔离的单元测试。
  • MSW 在网络层拦截请求,让你的实际 fetch 或 axios 逻辑执行,从而实现更真实的集成测试。
  • 当组件在更新 DOM 之前等待异步请求时,通常需要使用来自 @vue/test-utilsflushPromises()

为什么必须在 Vitest 的 Vue 组件测试中模拟 API 调用

测试中的真实网络调用会产生不确定的结果。同一个测试可能在本地通过,但在 CI 中失败,仅仅因为外部 API 速度慢或不可用。模拟让你控制网络返回的内容,使你的测试保持快速和可预测。

策略 1:使用 vi.mock 模拟 API 模块

最直接的方法是在组件看到它之前替换负责 HTTP 调用的模块。

// quote.service.ts
export async function fetchQuote() {
  const response = await fetch('https://api.example.com/quotes/random')
  return response.json()
}
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { vi, test, expect } from 'vitest'
import { fetchQuote } from './quote.service'
import QuoteCard from './QuoteCard.vue'

// vi.mock 会自动提升到导入语句之上
vi.mock('./quote.service')

test('displays a quote after mounting', async () => {
  vi.mocked(fetchQuote).mockResolvedValue({
    id: 1,
    quote: 'Test quote',
    author: 'Tester',
  })

  const wrapper = mount(QuoteCard)

  // 等待所有待处理的 promise 和 DOM 更新完成
  await flushPromises()

  expect(wrapper.text()).toContain('Test quote')
})

一个关键细节: vi.mock 会被 Vitest 提升到文件顶部,因此它在任何导入之前运行。这是 Vitest 模拟 API 中记录的预期行为。这意味着你可以安全地导入真实函数,但在测试中仍然会接收到模拟版本。

在这种模式中通常需要调用 flushPromises() 在挂载时获取数据的 Vue 组件会异步更新其 DOM。像 flushPromises 这样的工具确保在断言运行之前解决待处理的 promise,防止测试针对加载状态进行断言。

何时使用这种方法: 在隔离环境中对特定组件或服务进行单元测试时,你希望严格控制返回值和调用次数。

策略 2:在 Vitest Vue 测试中使用 MSW 进行 HTTP 层模拟

Mock Service Worker 在网络层拦截请求,而不是替换 JavaScript 模块。这使你的测试更加真实,因为实际的 fetch 或 axios 调用仍然会执行——MSW 只是在它离开进程之前拦截它。

安装 MSW 并为 Node 环境设置它(Vitest 的默认环境):

npm install -D msw
// test/server.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

export const server = setupServer(
  http.get('https://api.example.com/quotes/random', () => {
    return HttpResponse.json({ id: 1, quote: 'MSW quote', author: 'MSW' })
  })
)
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { test, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './test/server'
import QuoteCard from './QuoteCard.vue'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('displays a quote fetched via MSW', async () => {
  const wrapper = mount(QuoteCard)
  await flushPromises()
  expect(wrapper.text()).toContain('MSW quote')
})

出于同样的原因,这里可能仍然需要 flushPromises()——Vue 的响应式系统需要一个 tick 来在 promise 解决后应用 DOM 更新。

何时使用这种方法: 集成风格的测试,你希望验证组件与真实的 HTTP 行为一起工作,或者当多个组件共享同一个端点时。Vitest 自己的文档推荐在基于 Node 的测试环境中使用 MSW 进行请求级模拟。

vi.mock 和 MSW 之间选择

vi.mockMSW
拦截层级模块级别网络级别
测试真实的 fetch 逻辑
单个测试控制✅ 简单✅ 通过 server.use()
最适合单元测试集成测试

结论

这两种策略都是在 Vue 测试中模拟 API 调用的有效工具。当你想要快速、隔离的单元测试并精确控制返回值时,使用 vi.mock。当你希望测试执行完整的请求路径时,使用 MSW。无论哪种情况,像 flushPromises() 这样的工具都有助于确保 Vue 在断言运行之前完成渲染异步更新。

常见问题

可以。许多团队使用 vi.mock 对单个组件进行集中的单元测试,使用 MSW 进行跨越多个组件或服务的更广泛的集成测试。将它们放在单独的测试文件中,以避免混淆哪一层在处理模拟。

当你的断言运行时,Vue 可能仍在渲染加载状态。如果你的断言恰好匹配加载文本,测试会偶然通过。控制台警告通常表示未处理的 promise 拒绝或缺少 await。在挂载触发异步请求的组件后调用 flushPromises 有助于避免这些时序问题。

MSW 在网络层拦截,因此它适用于任何 HTTP 客户端,包括 axios、fetch 以及基于它们构建的库,如 ky 或 got。你选择的 HTTP 客户端不会影响 MSW 如何处理拦截。

使用 vi.mock 时,调用 mockRejectedValue 来模拟抛出的异步错误。使用 MSW 时,在特定测试中使用 server.use 注册一个返回带有 4xx 或 5xx 状态码的 HttpResponse.json 的处理程序。这两种方法都可以让你验证组件的错误处理。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay