测试

如何测试你的 Nuxt 应用。
如果你是模块作者,可以在 模块作者指南 中找到更具体的信息。

安装

为了让你管理其他测试依赖,@nuxt/test-utils 附带了各种可选的 peer 依赖。例如:

  • 你可以在运行时 Nuxt 环境中选择 happy-domjsdom
  • 你可以为端到端测试运行器选择 vitestcucumberjestplaywright
  • 仅当你希望使用内置的浏览器测试工具(且不使用 @playwright/test 作为测试运行器)时,才需要 playwright-core
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core

单元测试

我们目前为需要 Nuxt 运行时环境的代码提供了一个单元测试环境。当前「仅支持 vitest」(欢迎贡献以添加其他运行时)。

设置

  1. (可选)在你的 nuxt.config 文件中添加 @nuxt/test-utils/module。它会向 Nuxt DevTools 添加一个 Vitest 集成,支持在开发时运行你的单元测试。
    export default defineNuxtConfig({
      modules: [
        '@nuxt/test-utils/module',
      ],
    })
    
  2. 创建一个包含以下内容的 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/{e2e,unit}/*.{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 配置文件。

例如:vitest.config.m{ts,js}

可以使用 .env.test 文件为测试设置环境变量。

使用 Nuxt 运行时环境

使用 Vitest 项目,你可以精确控制哪些测试在何种环境中运行:

  • 单元测试:将常规单元测试放在 test/unit/ —— 这些在 Node 环境中运行以提高速度
  • Nuxt 测试:将依赖 Nuxt 运行时环境的测试放在 test/nuxt/ —— 这些将在 Nuxt 运行时环境中运行

可选:简单设置

如果你更喜欢更简单的设置并希望所有测试都在 Nuxt 环境中运行,可以使用基础配置:

import { defineVitestConfig } from '@nuxt/test-utils/config'

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' 的简单设置,你可以根据需要在每个测试文件中通过特殊注释选择退出 Nuxt 环境

// @vitest-environment node
import { test } from 'vitest'

test('my test', () => {
  // ... 在没有 Nuxt 环境的情况下测试!
})
不建议使用这种方法,因为它会创建一个混合环境,其中 Nuxt 的 Vite 插件会运行,但 Nuxt 入口和 nuxtApp 可能没有被初始化。这可能导致难以调试的错误。

组织你的测试

使用基于项目的设置,你可能会如下组织你的测试:

Directory structure
test/
├── e2e/
   └── ssr.test.ts
├── nuxt/
   ├── components.test.ts
   └── composables.test.ts
├── unit/
   └── utils.test.ts

当然你可以选择任意测试结构,但将 Nuxt 运行时环境与 Nuxt 端到端测试分开对测试稳定性很重要。

运行测试

使用项目设置,你可以运行不同的测试套件:

# 运行所有测试
npx vitest

# 仅运行单元测试
npx vitest --project unit

# 仅运行 Nuxt 测试
npx vitest --project nuxt

# 以监听模式运行测试
npx vitest --watch
当你在 Nuxt 环境中运行测试时,它们将运行在 happy-domjsdom 环境中。在测试运行之前,一个全局的 Nuxt 应用将被初始化(例如,会运行你在 app.vue 中定义的任何插件或代码)。这意味着你在测试中应该特别注意不要去改变全局状态(或者如果需要改变,测试后请务必重置它)。

🎭 内置模拟

@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 插件的注入。

在内部,mountSuspended 封装了来自 @vue/test-utilsmount,因此你可以查看 Vue Test Utils 文档 以了解可传入选项以及如何使用此工具。

例如:

// 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"',
  )
})
// 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>"
    `)
})

renderSuspended

renderSuspended 允许你在 Nuxt 环境中使用 @testing-library/vue 来渲染任意 Vue 组件,支持异步设置并能访问来自 Nuxt 插件的注入。

该方法应与 Testing Library 的实用工具(例如 screenfireEvent)一起使用。请在你的项目中安装 @testing-library/vue 以使用这些功能。

此外,Testing Library 还依赖测试全局变量来进行清理。你应在你的 Vitest 配置 中启用这些全局变量。

传入的组件将在一个 <div id="test-wrapper"></div> 内渲染。

示例:

// 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()
})
// 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>"
  `)
})

mockNuxtImport

mockNuxtImport 允许你模拟 Nuxt 的自动导入功能。例如,要模拟 useStorage,可以这样做:

import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport('useStorage', () => {
  return () => {
    return { value: 'mocked storage' }
  }
})

// your tests here
mockNuxtImport 在每个测试文件中每个被模拟的导入只能使用一次。它实际上是一个宏,会被转换为 vi.mock,而 vi.mock 会被提升,详见 Vitest 文档

如果你需要在不同测试之间为 Nuxt 导入提供不同的实现,可以通过创建并暴露你的模拟(使用 vi.hoisted)来实现,然后在 mockNuxtImport 中使用这些模拟。这样你可以访问被模拟的导入,并在测试之间更改实现。注意在每个测试前后恢复模拟状态以撤销模拟的状态更改(参见 restore mocks)。

import { vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

const { useStorageMock } = vi.hoisted(() => {
  return {
    useStorageMock: vi.fn(() => {
      return { value: 'mocked storage' }
    }),
  }
})

mockNuxtImport('useStorage', () => {
  return useStorageMock
})

// Then, inside a test
useStorageMock.mockImplementation(() => {
  return { value: 'something else' }
})

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'))

// your tests here

注意:工厂函数由于会被提升,不能在其中引用本地变量。如果你需要访问 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 方法。你可以通过将第二个参数设置为对象(而不是函数)来使用其他方法。

import { registerEndpoint } from '@nuxt/test-utils/runtime'

registerEndpoint('/test/', {
  method: 'POST',
  handler: () => ({ test: 'test-field' }),
})

注意:如果你的组件中的请求指向外部 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('useStorage', () => {
  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 composables、自动导入或上下文的组件,可以按以下步骤设置。

  1. 安装所需依赖
    npm i --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
    
  2. 创建一个包含以下内容的 vitest.config.ts
    import { defineConfig } from 'vitest/config'
    import vue from '@vitejs/plugin-vue'
    
    export default defineConfig({
      plugins: [vue()],
      test: {
        environment: 'happy-dom',
      },
    })
    
  3. 在你的 package.json 中添加一个测试脚本
    "scripts": {
      "build": "nuxt build",
      "dev": "nuxt dev",
      ...
      "test": "vitest"
    },
    
  4. 创建一个简单的 <HelloWorld> 组件 app/components/HelloWorld.vue,内容如下:
    <template>
      <p>Hello world</p>
    </template>
    
  5. 为这个新建组件创建一个简单的单元测试 ~/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')
      })
    })
    
  6. 运行 vitest 命令
    npm run test
    

恭喜,你现在已准备好在 Nuxt 中使用 @vue/test-utils 进行单元测试!祝测试愉快!

端到端测试

对于端到端测试,我们支持 VitestJestCucumberPlaywright 作为测试运行器。

设置

在每个使用 @nuxt/test-utils/e2e 辅助方法的 describe 块中,你需要在开始之前设置测试上下文。

test/my-test.spec.ts
import { describe, test } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'

describe('My test', async () => {
  await setup({
    // test context options
  })

  test('my test', () => {
    // ...
  })
})

在背后,setup 会在 beforeAllbeforeEachafterEachafterAll 中执行一系列任务,以正确设置 Nuxt 测试环境。

请使用下面列出的 setup 方法选项。

Nuxt 配置

  • rootDir:要进行测试的 Nuxt 应用目录路径。
    • 类型:string
    • 默认:'.'
  • configFile:配置文件的名称。
    • 类型:string
    • 默认:'nuxt.config'

时间设置

  • setupTimeout:允许运行 setupTest 完成工作的时间(毫秒),这可能包括为 Nuxt 应用构建或生成文件,具体取决于传入的选项。
    • 类型:number
    • 默认:120000(Windows 上为 240000
  • teardownTimeout:允许拆除测试环境(如关闭浏览器)所需的时间(毫秒)。
    • 类型:number
    • 默认:30000

功能选项

  • build:是否运行单独的构建步骤。
    • 类型:boolean
    • 默认:true(当 browserserver 被禁用,或提供了 host 时为 false
  • server:是否启动一个服务器以响应测试套件中的请求。
    • 类型:boolean
    • 默认:true(当提供 host 时为 false
  • port:如果提供,将把启动的测试服务器端口设置为该值。
    • 类型:number | undefined
    • 默认:undefined
  • host:如果提供,则使用该 URL 作为测试目标,而不是构建并运行新的服务器。适用于对已部署的应用或已在本地运行的服务器进行“真实”的端到端测试(通常能显著减少测试执行时间)。参见下面的 目标主机端到端示例
    • 类型:string
    • 默认:undefined
  • browser:在底层,Nuxt 测试工具使用 playwright 进行浏览器测试。如果设置此选项,将会启动一个浏览器,并可在随后测试套件中进行控制。
    • 类型:boolean
    • 默认:false
  • browserOptions
    • 类型:包含以下属性的 object
      • type:要启动的浏览器类型 —— chromiumfirefoxwebkit
      • launch:在启动浏览器时将传递给 playwright 的选项对象。参见 完整 API 参考
  • runner:为测试套件指定运行器。目前建议使用 Vitest
    • 类型:'vitest' | 'jest' | 'cucumber'
    • 默认:'vitest'
目标 host 端到端示例

端到端测试的常见用例是在与你通常用于生产的环境相同的环境中对已部署的应用运行测试。

在本地开发或自动部署流水线中,对一个单独的本地服务器进行测试通常更加高效,并且通常比在测试间让测试框架重新构建要快很多。

要为端到端测试使用单独的目标主机,只需在 setup 函数中提供所需 URL 的 host 属性。

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 } = res

url(path)

获取给定页面的完整 URL(包括测试服务器运行的端口)。

import { url } from '@nuxt/test-utils/e2e'

const pageUrl = url('/page')
// 'http://localhost:6840/page'

在浏览器中测试

我们在 @nuxt/test-utils 中为 Playwright 提供了内置支持,可以以编程方式或通过 Playwright 测试运行器使用。

createPage(url)

vitestjestcucumber 中,你可以使用 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-utils

你可以提供全局 Nuxt 配置,格式与前面提到的 setup() 函数相同。

playwright.config.ts
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)),
    },
  },
  // ...
})
Read more in 查看完整示例配置.

然后你的测试文件应该直接从 @nuxt/test-utils/playwright 使用 expecttest

tests/example.test.ts
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 服务器:

tests/example.test.ts
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!')
})