测试工具
Maschinelle Übersetzung ·0 % geprüft
Nuxt 通过 @nuxt/test-utils 为 Nuxt 应用提供一流的端到端和单元测试支持。它是一个测试工具和配置库,目前支持我们在 Nuxt 本身中使用的测试,以及整个模块生态系统中的测试。
安装
为了方便你管理其他测试依赖,@nuxt/test-utils 附带了各种可选的同伴依赖。例如:
- 你可以选择
happy-dom或jsdom作为 Nuxt 运行时环境 - 你可以选择
vitest、cucumber、jest和playwright作为端到端测试运行器 - 只有当你想使用内置的浏览器测试工具(且不使用
@playwright/test作为测试运行器)时,才需要playwright-core
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-coreyarn add --dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-corepnpm add -D @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-corebun add --dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core单元测试
我们目前提供了一个适用于需要 Nuxt 运行时环境的代码的单元测试环境。目前仅支持 vitest(不过欢迎贡献,添加对其他运行器的支持)。
设置
(可选)向你的
nuxt.config文件中添加@nuxt/test-utils/module。它为 Nuxt DevTools 添加了 Vitest 集成,支持在开发时运行单元测试。export default defineNuxtConfig({ modules: [ '@nuxt/test-utils/module', ], })新建
vitest.config.ts,内容如下:import { defineConfig } from 'vitest/config' import { defineVitestProject } from '@nuxt/test-utils/config' export default defineConfig({ test: { projects: [ { test: { name: 'unit', include: ['test/unit/*.{test,spec}.ts'], environment: 'node', }, }, { test: { name: 'e2e', include: ['test/e2e/*.{test,spec}.ts'], environment: 'node', }, }, await defineVitestProject({ test: { name: 'nuxt', include: ['test/nuxt/*.{test,spec}.ts'], environment: 'nuxt', }, }), ], }, })
在你的 Vitest 配置中导入 @nuxt/test-utils 时,package.json 中必须指定 "type": "module",或者将配置文件重命名为适当的名称,
例如:vitest.config.m{ts,js}。.env.test 文件设置测试环境变量。使用 Nuxt 运行时环境
默认情况下,@nuxt/test-utils 不会更改默认的 Vitest 环境。你可以精细地选择是否启用 Nuxt 环境,并且可以将 Nuxt 测试与其他单元测试一起运行。
你可以通过在测试文件名中添加 .nuxt.(例如 my-file.nuxt.test.ts 或 my-file.nuxt.spec.ts)来选择使用 Nuxt 环境,或者在测试文件中直接添加注释 @vitest-environment nuxt。
另一种方式:简单配置
如果你更倾向于让所有测试都在 Nuxt 环境中运行,可以使用如下基础配置:
import { defineVitestConfig } from '@nuxt/test-utils/config'
import { fileURLToPath } from 'node:url'
export default defineVitestConfig({
test: {
environment: 'nuxt',
// 你还可以可选设置 Nuxt 特有的环境选项
// environmentOptions: {
// nuxt: {
// rootDir: fileURLToPath(new URL('./playground', import.meta.url)),
// domEnvironment: 'happy-dom', // 'happy-dom'(默认)或 'jsdom'
// overrides: {
// // 你要传入的其他 Nuxt 配置
// }
// }
// }
},
})如果你默认设置了 environment: 'nuxt',也可以根据需要在每个测试文件中通过注释选择退出默认环境。
// @vitest-environment node
import { test } from 'vitest'
test('my test', () => {
// ... 不使用 Nuxt 环境的测试!
})当你在 Nuxt 环境中运行测试时,测试实际上是在 happy-dom 或 jsdom 环境中执行的。运行前会初始化全局 Nuxt app(包括例如执行你在 app.vue 中定义的插件或代码)。
这意味着你在测试中应特别注意不要修改全局状态(如果必须修改,务必在测试后重置)。
🎭 内置的 Mock
@nuxt/test-utils 为 DOM 环境提供了一些内置的模拟。
intersectionObserver
默认值为 true,创建一个不含任何功能的 IntersectionObserver API 虚拟类。
indexedDB
默认值为 false,使用 fake-indexeddb 创建 IndexedDB API 的功能性模拟。
这些可以在你的 vitest.config.ts 文件的 environmentOptions 部分配置:
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environmentOptions: {
nuxt: {
mock: {
intersectionObserver: true,
indexedDb: true,
},
},
},
},
})🛠️ 辅助函数
@nuxt/test-utils 提供了多个辅助函数,方便测试 Nuxt 应用。
mountSuspended
mountSuspended 允许你在 Nuxt 环境中挂载任意 Vue 组件,支持异步设置,并能访问来自你的 Nuxt 插件的依赖注入。
@vue/test-utils 的 mount,更多选项及用法请参考Vue Test Utils 文档。示例:
// @noErrors
import { expect, it } from 'vitest'
import type { Component } from 'vue'
declare module '#components' {
export const SomeComponent: Component
}
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { SomeComponent } from '#components'
it('can mount some component', async () => {
const component = await mountSuspended(SomeComponent)
expect(component.text()).toMatchInlineSnapshot(
'"This is an auto-imported component"',
)
})// @noErrors
import { expect, it } from 'vitest'
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import App from '~/app.vue'
// tests/App.nuxt.spec.ts
it('can also mount an app', async () => {
const component = await mountSuspended(App, { route: '/test' })
expect(component.html()).toMatchInlineSnapshot(`
"<div>This is an auto-imported component</div>
<div> I am a global component </div>
<div>/</div>
<a href="/test"> Test link </a>"
`)
})选项对象接受 @vue/test-utils 的 mount 选项以及以下属性:
route:初始路由,或false用于跳过初始路由切换(默认/)。
renderSuspended
renderSuspended 允许你使用 @testing-library/vue 在 Nuxt 环境中渲染任意 Vue 组件,支持异步设置并访问 Nuxt 插件中的注入。
配合 Testing Library 的工具(如 screen 和 fireEvent)一起使用。你需要在项目中安装 @testing-library/vue。
此外,Testing Library 还依赖用于清理(cleanup)的测试全局变量。你应在 Vitest 配置中开启它们。
传入的组件将渲染在 <div id="test-wrapper"></div> 内。
示例:
// @noErrors
import { expect, it } from 'vitest'
import type { Component } from 'vue'
declare module '#components' {
export const SomeComponent: Component
}
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { SomeComponent } from '#components'
import { screen } from '@testing-library/vue'
it('can render some component', async () => {
await renderSuspended(SomeComponent)
expect(screen.getByText('This is an auto-imported component')).toBeDefined()
})// @noErrors
import { expect, it } from 'vitest'
// ---cut---
// tests/App.nuxt.spec.ts
import { renderSuspended } from '@nuxt/test-utils/runtime'
import App from '~/app.vue'
it('can also render an app', async () => {
const html = await renderSuspended(App, { route: '/test' })
expect(html).toMatchInlineSnapshot(`
"<div id="test-wrapper">
<div>This is an auto-imported component</div>
<div> I am a global component </div>
<div>Index page</div><a href="/test"> Test link </a>
</div>"
`)
})选项对象接受 @testing-library/vue 的 render 选项以及以下属性:
route:初始路由,或false用于跳过初始路由切换(默认/)。
mockNuxtImport
mockNuxtImport 允许你模拟 Nuxt 的自动导入功能。例如,要模拟 useState,你可以这样做:
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
mockNuxtImport('useState', () => {
return () => {
return { value: 'mocked storage' }
}
})
// 你的测试代码你可以显式为 mock 标注类型以保证类型安全,并在模拟复杂功能时使用工厂函数传入的原始实现。
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
mockNuxtImport<typeof useState>('useState', (original) => {
return (...args) => {
return { ...original('some-key'), value: 'mocked state' }
}
})
// 或者指定要模拟的目标
mockNuxtImport(useState, (original) => {
return (...args) => {
return { ...original('some-key'), value: 'mocked state' }
}
})
// 你的测试mockNuxtImport 在每个测试文件中,每个被模拟的导入只能使用一次。它实际上是一个宏,会转换成 vi.mock,而 vi.mock 会被提升,详见Vitest 文档。如果你想在不同测试间为同一 Nuxt 导入提供不同实现,可以使用 vi.hoisted 创建并导出模拟,然后在 mockNuxtImport 中使用它们。这样你就可以访问模拟的导入,并在测试间切换实现。注意在每个测试前后调用 restore mocks,以还原 mock 状态变化。
import { vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const { useStateMock } = vi.hoisted(() => {
return {
useStateMock: vi.fn(() => {
return { value: 'mocked storage' }
}),
}
})
mockNuxtImport('useState', () => {
return useStateMock
})
// 然后,在某个测试内部
useStateMock.mockImplementation(() => {
return { value: 'something else' }
})如果你需要只在某个测试内部模拟行为,也可以使用下面这种方式。
import { beforeEach, vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
mockNuxtImport(useRoute, original => vi.fn(original))
beforeEach(() => {
vi.resetAllMocks()
})
// 然后,在某个测试内部
const useRouteOriginal = vi.mocked(useRoute).getMockImplementation()!
vi.mocked(useRoute).mockImplementation(
(...args) => ({ ...useRouteOriginal(...args), path: '/mocked' }),
)mockComponent
mockComponent 允许你模拟 Nuxt 组件。第一个参数可以是组件名(PascalCase),也可以是组件的相对路径。第二个参数为返回模拟组件的工厂函数。
例如,模拟 MyComponent:
import { mockComponent } from '@nuxt/test-utils/runtime'
mockComponent('MyComponent', {
props: {
value: String,
},
setup (props) {
// ...
},
})
// 相对路径或别名同样支持
mockComponent('~/components/my-component.vue', () => {
// 或工厂函数形式
return defineComponent({
setup (props) {
// ...
},
})
})
// 也可以用 SFC 形式重定向到模拟组件
mockComponent('MyComponent', () => import('./MockComponent.vue'))
// 你的测试代码注意:工厂函数内不能引用本地变量,因为它们会被提升。如果需要访问 Vue API 或其他变量,须在工厂函数内导入。import { mockComponent } from '@nuxt/test-utils/runtime'
mockComponent('MyComponent', async () => {
const { ref, h } = await import('vue')
return defineComponent({
setup (props) {
const counter = ref(0)
return () => h('div', null, counter.value)
},
})
})registerEndpoint
registerEndpoint 用于创建返回模拟数据的 Nitro 接口。如果你需要测试向 API 请求数据并显示的组件,这很有用。
第一个参数是接口路径(例如 /test/),第二个参数是返回模拟数据的工厂函数。
例如,模拟 /test/ 接口:
import { registerEndpoint } from '@nuxt/test-utils/runtime'
registerEndpoint('/test/', () => ({
test: 'test-field',
}))默认请求方法为 GET。若需要使用其他方法,可以传入一个带有 method 和 handler 的对象替代函数:
import { registerEndpoint } from '@nuxt/test-utils/runtime'
registerEndpoint('/test/', {
method: 'POST',
handler: () => ({ test: 'test-field' }),
})这个对象接受以下属性:
handler:事件处理函数method:(可选)要匹配的 HTTP 方法(例如 'GET'、'POST')once:(可选)如果为 true,处理程序将只用于第一次匹配的请求,然后会被自动移除
注意:如果组件中的请求指向外部 API,你可以使用 baseURL,然后通过 Nuxt 环境覆盖配置($test)将其设为空,这样所有请求都会发送到 Nitro 服务器。@nuxt/test-utils/runtime 与 @nuxt/test-utils/e2e 需要运行在不同的测试环境中,不能在同一个文件中混用。
如果你想同时使用 @nuxt/test-utils 的端到端和单元测试功能,可以将测试拆分到不同文件,然后通过特殊注释 // @vitest-environment nuxt 或以 .nuxt.spec.ts 为后缀名来分别指定运行环境。
app.nuxt.spec.ts
import { mockNuxtImport } from '@nuxt/test-utils/runtime'
mockNuxtImport('useState', () => {
return () => {
return { value: 'mocked storage' }
}
})app.e2e.spec.ts
import { $fetch, setup } from '@nuxt/test-utils/e2e'
await setup({
setupTimeout: 10000,
})
// ...使用 @vue/test-utils
如果你更喜欢单独使用 @vue/test-utils 做 Nuxt 单元测试,并且仅测试不依赖 Nuxt 组合式函数、自动导入或上下文的组件,可以按以下步骤配置。
安装依赖
npm i --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vueyarn add --dev vitest @vue/test-utils happy-dom @vitejs/plugin-vuepnpm add -D vitest @vue/test-utils happy-dom @vitejs/plugin-vuebun add --dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue创建
vitest.config.ts:import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'happy-dom', }, })在
package.json中添加测试命令:"scripts": { "build": "nuxt build", "dev": "nuxt dev", ... "test": "vitest" },创建简单的
<HelloWorld>组件components/HelloWorld.vue:<template> <p>Hello world</p> </template>创建该组件的简单单元测试
~/components/HelloWorld.spec.ts:import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import HelloWorld from './HelloWorld.vue' describe('HelloWorld', () => { it('component renders Hello world properly', () => { const wrapper = mount(HelloWorld) expect(wrapper.text()).toContain('Hello world') }) })运行 Vitest 命令
npm run testyarn testpnpm run testbun run test
恭喜!你现在已经可以开始在 Nuxt 中使用 @vue/test-utils 进行单元测试了,祝测试愉快!
端到端测试
对于端到端测试,我们支持 Vitest、Jest、Cucumber 和 Playwright 作为测试运行器。
设置
在每个使用 @nuxt/test-utils/e2e 辅助方法的 describe 区块里,你需要在开始前先设置测试上下文。
import { describe, test } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
describe('My test', async () => {
await setup({
// 测试上下文选项
})
test('my test', () => {
// ...
})
})setup 会在内部通过 beforeAll、beforeEach、afterEach 和 afterAll 执行多项操作,以正确建立 Nuxt 测试环境。
请使用下面的参数配置 setup。
Nuxt 配置
rootDir:被测试的 Nuxt 应用所在目录路径。- 类型:
string - 默认:
'.'
- 类型:
configFile:配置文件名。- 类型:
string - 默认:
'nuxt.config'
- 类型:
时间设置
setupTimeout:允许setupTest完成(如构建或生成 Nuxt 应用文件)所耗费的最大时间,单位为毫秒。- 类型:
number - 默认:
120000(Windows 系统上为240000)
- 类型:
teardownTimeout:允许拆除测试环境(如关闭浏览器)所耗费的最大时间,单位为毫秒。- 类型:
number - 默认:
30000
- 类型:
功能配置
build:是否执行单独构建步骤。- 类型:
boolean - 默认:
true(如果禁用browser或server,或提供了host,则为false)
- 类型:
server:是否启动服务器响应测试请求。- 类型:
boolean - 默认:
true(如果提供了host,则为false)
- 类型:
port:指定启动的测试服务器端口。- 类型:
number | undefined - 默认:
undefined
- 类型:
host:指定测试目标 URL,以代替构建和运行新服务器。适用于对生产环境或本地已运行的服务器进行真实端到端测试,有助于缩短测试耗时。详见下文目标 host 端到端示例。- 类型:
string - 默认:
undefined
- 类型:
browser:在底层,Nuxt 测试工具使用playwright进行浏览器测试。如果设置为true,将在测试中启动浏览器并可对其进行控制。- 类型:
boolean - 默认:
false
- 类型:
host:如果提供,则使用该 URL 作为测试目标,而不构建和运行新服务器。适用于对已部署的应用版本或本地已运行的服务器进行“真实”的端到端测试(这可能会显著缩短测试运行时间)。请参阅下文的目标 host 端到端示例。- 类型:
string - 默认:
undefined
- 类型:
browser:在底层,Nuxt 测试工具使用playwright执行浏览器测试。如果设置此选项,将启动一个浏览器,并可在后续测试套件中对其进行控制。- 类型:
boolean - 默认:
false
- 类型:
browserOptions类型:对象,包含以下属性
type:要启动的浏览器类型,chromium、firefox或webkitlaunch:启动浏览器时传递给 playwright 的参数对象,详见完整 API
runner:指定测试套件使用的运行器。目前推荐使用 Vitest。- 类型:
'vitest' | 'jest' | 'cucumber' - 默认:
'vitest'
- 类型:
目标 host 端到端示例
端到端测试的常见场景是在通常用于生产的环境中运行已部署的应用。
在本地开发或自动部署管道中,使用单独的本地服务器进行测试会更高效,而且通常比让测试框架在测试之间重新构建更快。
要使用单独的目标服务器,只需给 setup 函数传入想要的 host URL。
import { createPage, setup } from '@nuxt/test-utils/e2e'
import { describe, expect, it } from 'vitest'
describe('login page', async () => {
await setup({
host: 'http://localhost:8787',
})
it('displays the email and password fields', async () => {
const page = await createPage('/login')
expect(await page.getByTestId('email').isVisible()).toBe(true)
expect(await page.getByTestId('password').isVisible()).toBe(true)
})
})API
$fetch(url)
获取服务端渲染页面的 HTML。
import { $fetch } from '@nuxt/test-utils/e2e'
const html = await $fetch('/')fetch(url)
获取服务端渲染页面的响应对象。
import { fetch } from '@nuxt/test-utils/e2e'
const res = await fetch('/')
const { body, headers } = resurl(path)
获取指定页面的完整 URL(含测试服务器端口)。
import { url } from '@nuxt/test-utils/e2e'
const pageUrl = url('/page')
// 'http://localhost:6840/page'浏览器中的测试
我们为 @nuxt/test-utils 内置了使用 Playwright 的支持,既可编程使用,也可使用 Playwright 自带的测试运行器。
createPage(url)
在 vitest、jest 或 cucumber 中,可用 createPage 创建配置好的 Playwright 浏览器实例,并(可选)指定运行服务器的路径。详情参见 Playwright API 文档。
import { createPage } from '@nuxt/test-utils/e2e'
const page = await createPage('/page')
// 你可以从 page 变量访问 Playwright 的所有 API使用 Playwright 测试运行器
我们也为使用 Playwright 测试运行器测试 Nuxt 提供一流支持。
npm i --save-dev @playwright/test @nuxt/test-utilsyarn add --dev @playwright/test @nuxt/test-utilspnpm add -D @playwright/test @nuxt/test-utilsbun add --dev @playwright/test @nuxt/test-utilsdeno add --dev npm:@playwright/test npm:@nuxt/test-utils你可以提供全局 Nuxt 配置,内容与前面 setup() 函数的配置相同。
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'
export default defineConfig<ConfigOptions>({
use: {
nuxt: {
rootDir: fileURLToPath(new URL('.', import.meta.url)),
},
},
// ...
})测试文件中应直接从 @nuxt/test-utils/playwright 导入 expect 和 test:
import { expect, test } from '@nuxt/test-utils/playwright'
test('test', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})你也可以直接在测试文件中配置 Nuxt 服务:
import { expect, test } from '@nuxt/test-utils/playwright'
test.use({
nuxt: {
rootDir: fileURLToPath(new URL('..', import.meta.url)),
},
})
test('test', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})