跳到主要内容

Testing

如何测试你的 Nuxt 应用。

如果你是模块作者,可以在 Module Author's guide 中找到更具体的信息。

Nuxt 通过 @nuxt/test-utils 为你的 Nuxt 应用提供了一流的端到端(end-to-end)和单元测试支持。@nuxt/test-utils 是一个测试工具和配置库,目前为 我们在 Nuxt 自身上使用的测试 以及整个模块生态系统中的测试提供动力。

安装(Installation)

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

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

单元测试(Unit Testing)

我们目前提供了一个用于需要 Nuxt 运行时环境的代码单元测试的环境。它目前_仅支持 vitest_(尽管欢迎贡献以添加其他运行时)。

设置(Setup)

  1. @nuxt/test-utils/module 添加到你的 nuxt.config 文件(可选)。它会将 Vitest 集成添加到你的 Nuxt DevTools 中,支持在开发中运行你的单元测试。
    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/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',
            },
          }),
        ],
      },
    })
    
  3. 如果你的 Nuxt 环境测试位于 test/nuxt/ 之外,请参阅 测试中的 TypeScript 支持 将它们添加到 TypeScript 上下文中。
在 vitest 配置中导入 @nuxt/test-utils 时,必须在你的 package.json 中指定 "type": "module",或者适当地重命名你的 vitest 配置文件。

vitest.config.m{ts,js}

可以通过使用 .env.test 文件来设置用于测试的 environment 变量。

使用 Nuxt 运行时环境(Using a Nuxt Runtime Environment)

使用 Vitest projects,你可以精细控制哪些测试运行在哪个环境中:

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

替代方案:简单设置(Alternative: Simple Setup)

如果你更喜欢更简单的设置,并希望所有测试都在 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' 作为默认值的简单设置,可以根据需要为每个测试文件_退出_ Nuxt 环境

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

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

组织你的测试(Organizing Your Tests)

通过基于项目的设置,你可以这样组织你的测试:

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

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

测试中的 TypeScript 支持(TypeScript Support in Tests)

默认情况下,test/nuxt/tests/nuxt/ 目录中的测试文件被包含在 Nuxt 应用 TypeScript 上下文 中。这意味着它们会识别 Nuxt 别名(如 ~/@/#imports),并且 TypeScript 会感知在你的 Nuxt 应用中工作的自动导入。

这与推荐的结构相匹配,即只有需要 Nuxt 运行时环境的测试才放在这些目录中。其他目录(如 test/unit/)中的单元测试可以根据需要手动添加。
添加其他测试目录(Adding other test directories)

如果你在其他目录中有将在 Nuxt Vitest 环境中运行的测试,可以通过将它们在你的配置中添加进来,使其包含在 Nuxt 应用 TypeScript 上下文中:

nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    tsConfig: {
      include: [
        // 此路径相对于生成的 .nuxt/tsconfig.json
        '../test/other-nuxt-context/**/*',
      ],
    },
  },
})

单元测试不应依赖像自动导入或 composable 这样的 Nuxt 运行时特性。仅当你的测试从你的源文件导入(例如 ~/utils/helpers)时,才添加 TypeScript 路径别名支持,而不是为了 Nuxt 特定特性。

运行测试(Running Tests)

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

# 运行所有测试
npx vitest

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

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

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

🎭 内置 Mock(Built-In Mocks)

@nuxt/test-utils 为 DOM 环境提供了一些内置 mock。

intersectionObserver

默认 true,为 IntersectionObserver API 创建一个没有任何功能的虚拟类

indexedDB

默认 false,使用 fake-indexeddb 创建 IndexedDB API 的功能性 mock

这些可以在你的 vitest.config.ts 文件的 environmentOptions 部分进行配置:

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

export default defineVitestConfig({
  test: {
    environmentOptions: {
      nuxt: {
        mock: {
          intersectionObserver: true,
          indexedDb: true,
        },
      },
    },
  },
})

🛠️ 辅助函数(Helpers)

@nuxt/test-utils 提供了许多辅助函数,让测试 Nuxt 应用更容易。

mountSuspended

mountSuspended 允许你在 Nuxt 环境中挂载任何 Vue 组件,允许 async setup 并访问来自你 Nuxt 插件的注入。

在底层,mountSuspended 包裹了 @vue/test-utilsmount,所以你可以查看 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>"
    `)
})

options 对象接受 @vue/test-utils 的 mount 选项以及以下属性:

  • route:初始路由,或 false 以跳过初始路由变更(默认 /)。

renderSuspended

renderSuspended 允许你使用 @testing-library/vue 在 Nuxt 环境中渲染任何 Vue 组件,允许 async setup 并访问来自你 Nuxt 插件的注入。

这应该与 Testing Library 的工具一起使用,例如 screenfireEvent。在你的项目中安装 @testing-library/vue 以使用它们。

此外,Testing Library 还依赖测试全局变量来进行清理。你应该在你的 Vitest config 中开启它们。

传入的组件会渲染在 <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>"
  `)
})

options 对象接受 @testing-library/vue 的 render 选项以及以下属性:

  • route:初始路由,或 false 以跳过初始路由变更(默认 /)。

mockNuxtImport

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

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

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

// your tests here

你可以显式地为 mock 指定类型以获得类型安全,并在 mock 复杂功能时使用传入工厂函数的原始实现。

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

mockNuxtImport<typeof useState>('useState', (original) => {
  return (...args) => {
    return { ...original('some-key'), value: 'mocked state' }
  }
})

// 或者指定要 mock 的目标
mockNuxtImport(useState, (original) => {
  return (...args) => {
    return { ...original('some-key'), value: 'mocked state' }
  }
})

// your tests here
mockNuxtImport 在每个测试文件中对每个被 mock 的导入只能使用一次。它实际上是一个宏,会被转换为 vi.mock,而 vi.mock 会被提升(hoisted),如 Vitest 文档 所述。

如果你需要 mock 一个 Nuxt 导入并在测试之间提供不同的实现,你可以通过使用 vi.hoisted 创建并暴露你的 mock,然后在 mockNuxtImport 中使用那些 mock。这样你就有了对被 mock 导入的访问权,并可以在测试之间更改实现。请注意在每次测试之前或之后恢复 mock 以撤销 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' }
})

如果你只需要在测试内部 mock 行为,也可以使用以下方法。

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 允许你 mock Nuxt 的组件。 第一个参数可以是 PascalCase 形式的组件名,或者组件的相对路径。 第二个参数是一个返回被 mock 组件的工厂函数。

例如,要 mock 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 重定向到一个 mock 组件
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 允许你创建返回 mock 数据的 Nitro 端点。如果你想测试一个向 API 发起请求以展示某些数据的组件,它会派上用场。

第一个参数是端点名称(例如 /test/)。 第二个参数是一个返回 mock 数据的工厂函数。

例如,要 mock /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' }),
})

这个对象接受以下属性:

  • handler:事件处理函数
  • method:(可选)要匹配的 HTTP 方法(例如 'GET'、'POST')
  • once:(可选)如果为 true,该处理函数只会用于第一次匹配的请求,然后自动移除

注意:如果你的组件中的请求发往外部 API,你可以使用 baseURL,然后使用 Nuxt 环境覆盖配置$test)将其设为空,这样你所有的请求都会发往 Nitro 服务器。

与端到端测试的冲突(Conflict with End-To-End Testing)

@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 composable、自动导入或上下文的组件,你可以按照以下步骤进行设置。

  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 中添加一个新的 test 命令
    "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 进行单元测试了!测试愉快!

端到端测试(End-To-End Testing)

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

设置(Setup)

在每个使用 @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('my test', () => {
    // ...
  })
})

在底层,setup 会在 beforeAllbeforeEachafterEachafterAll 中执行多项任务,以正确设置 Nuxt 测试环境。

请使用以下 setup 方法的选项。

Nuxt 配置(Nuxt Config)

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

计时(Timings)

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

特性(Features)

  • build:是否运行单独的构建步骤。
    • 类型:boolean
    • 默认:true(如果禁用了 browserserver,或者提供了 host,则为 false
  • server:是否启动一个服务器来响应测试套件中的请求。
    • 类型:boolean
    • 默认:true(如果提供了 host,则为 false
  • port:如果提供,将启动的测试服务器端口设为该值。
    • 类型:number | undefined
    • 默认:undefined
  • host:如果提供,用作测试目标的 URL,而不是构建并运行一个新的服务器。适用于针对已部署版本(通常在与生产相同的环境中运行)或已运行的本地服务器运行「真实」的端到端测试(这可能会显著减少测试执行时间)。请参阅下面的 目标 host 端到端示例
    • 类型:string
    • 默认:undefined
  • browser:在底层,Nuxt test utils 使用 playwright 执行浏览器测试。如果设置了此选项,将启动一个浏览器,并可在后续测试套件中控制它。
    • 类型:boolean
    • 默认:false
  • browserOptions
    • 类型:包含以下属性的 object
      • type:要启动的浏览器类型 - chromiumfirefoxwebkit
      • launch:启动浏览器时传给 playwright 的选项 object。请参阅 完整 API 参考
  • runner:指定测试套件的运行器。目前推荐 Vitest
    • 类型:'vitest' | 'jest' | 'cucumber'
    • 默认:'vitest'
目标 host 端到端示例(Target host end-to-end example)

端到端测试的一个常见用例是针对通常在生产环境中使用的同一环境中运行的已部署应用运行测试。

对于本地开发或自动化部署流水线,针对一个独立的本地服务器进行测试可能更高效,并且通常比让测试框架在测试之间重建要快。

要利用一个独立的目标 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'

在浏览器中测试(Testing in a Browser)

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

createPage(url)

vitestjestcucumber 中,你可以通过 createPage 创建一个配置好的 Playwright 浏览器实例,并(可选地)将其指向运行中服务器的某个路径。你可以从 Playwright 文档 中了解更多可用的 API 方法。

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

const page = await createPage('/page')
// 你可以通过 `page` 变量访问所有 Playwright API

使用 Playwright 测试运行器测试(Testing with Playwright Test Runner)

我们也提供在 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)),
    },
  },
  // ...
})
查看完整示例配置·阅读更多

你的测试文件随后应直接从 @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!')
})