进阶 vitest.dev 2026-10-22 19:00:41 · 20 阅读

第7章 Vitest 测试标签详解

第7章 测试标签

Test Tags 4.1.0+

标签允许你为测试打标,以便过滤需要运行的测试,并在必要时覆盖其运行选项。

为什么需要标签

当测试套件中包含共享某些运行选项的测试组时,标签就会派上用场。例如,数据库查询可能需要更长的超时时间,而 CI 上的集成测试可能需要重试。手动在每个相关测试上重复设置这些选项非常脆弱,且这些分类通常与文件路径不对应,因此按文件拆分行不通。特别是那些不稳定的(flaky)测试,它们往往聚集在产生 bug 的地方,而不是统一的 flaky/ 目录下。

标签能捕捉这类分类:定义中包含共享选项,任何标记了该标签的测试都会继承这些选项。标签名称还可以组合成表达式:--tags-filter='db && !flaky' 会运行那些没有标记为 flaky 的数据库测试。TestRunner.matchesTags 在运行时暴露了相同的表达式,这在 globalSetup 执行昂贵操作且应在没有相应标签的测试被调度时跳过的场景下非常有用。

何时使用标签

如果你有需求……使用:

  • 对某类测试应用超时/重试 → 标签
  • 标记分散在许多文件中的交叉分类(flaky、slow、frontend) → 标签
  • 根据过滤内容有条件地运行昂贵设置 → 标签 + matchesTags
  • 按测试名称运行子集 → -t / testNamePattern
  • 按文件路径运行子集 → --include / --exclude
  • 用不同的运行设置(隔离、池、环境)运行不同文件 → 测试项目(Test Projects)

你可以结合使用项目和标签。位于 Sequential 项目中的测试也可以携带 flaky 标签,Vitest 会同时应用两者。

定义标签

标签必须在配置文件中定义。默认情况下,Vitest 不提供任何内置标签。如果测试使用了未在配置中定义的标签,测试运行器将抛出错误。这防止了因标签名称拼写错误导致的不预期行为。你可以使用 strictTags 选项禁用此检查。

你必须有定义标签的名称,也可以定义将应用于标记该标签的所有测试的额外选项,例如超时或重试。关于可用选项的完整列表,请参阅 tags。

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

export default defineConfig({
  test: {
    tags: [
      {
        name: 'frontend',
        description: '前端编写的测试。',
      },
      {
        name: 'backend',
        description: '后端编写的测试。',
      },
      {
        name: 'db',
        description: '数据库查询测试。',
        timeout: 60_000,
      },
      {
        name: 'flaky',
        description: '不稳定的 CI 测试。',
        retry: process.env.CI ? 3 : 0,
        timeout: 30_000,
        priority: 1,
      },
    ],
  },
})

如果你使用 TypeScript,可以通过扩展 TestTags 类型来强制可用的标签,其中包含一个字符串联合属性(确保该文件被 tsconfig 包含):

vitest.shims.ts
import 'vitest'

declare module 'vitest' {
  interface TestTags {
    tags:
      | 'frontend'
      | 'backend'
      | 'db'
      | 'flaky'
  }
}

要查看所有标签,可以使用 --list-tags 命令:

vitest --list-tags

frontend: 前端编写的测试。
backend: 后端编写的测试。
db: 数据库查询测试。
flaky: 不稳定的 CI 测试。

若要打印为 JSON,传入 --list-tags=json:

{
  "tags": [
    {
      "name": "frontend",
      "description": "前端编写的测试。"
    },
    {
      "name": "backend",
      "description": "后端编写的测试。"
    },
    {
      "name": "db",
      "description": "数据库查询测试。",
      "timeout": 60000
    },
    {
      "name": "flaky",
      "description": "不稳定的 CI 测试。",
      "retry": 0,
      "timeout": 30000,
      "priority": 1
    }
  ],
  "projects": []
}

解析选项冲突

如果多个标签定义了相同的选项并应用于同一个测试,首先按优先级解析(数字越小优先级越高),然后按它们在测试 tags 数组中的出现顺序解析。没有优先级的标签先合并,并被更高优先级的标签覆盖:

test('flaky database test', { tags: ['flaky', 'db'] })
// { timeout: 30_000, retry: 3 }

超时时间为 30 秒(而不是 60 秒),因为 flaky 的优先级为 1,而 db 没有优先级。

定义在测试本身的选项总是优先:

test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 })
// { timeout: 120_000, retry: 3 }

设置为 undefined 的选项会被忽略,因此不会覆盖来自标签的值:

test('flaky database test', { tags: ['flaky', 'db'], timeout: undefined })
// { timeout: 30_000, retry: 3 }

在测试中使用标签

你可以使用 tags 选项将标签应用于单个测试或整个测试套件:

import { describe, test } from 'vitest'

test('renders homepage', { tags: ['frontend'] }, () => {
  // ...
})

describe('API endpoints', { tags: ['backend'] }, () => {
  test('returns user data', () => {
    // 此测试从父级测试套件继承 "backend" 标签
  })

  test('validates input', { tags: ['validation'] }, () => {
    // 此测试具有 "backend"(继承)和 "validation" 标签
  })
})

标签会从父级测试套件继承,因此标记了标签的 describe 块内的所有测试都会自动拥有该标签。

你还可以通过在文件顶部使用 JSDoc 的 @module-tag 为文件中的每个测试定义标签:

/**
 * Auth 测试
 * @module-tag admin/pages/dashboard
 * @module-tag acceptance
 */

test('dashboard renders items', () => {
  // ...
})

危险:JSDoc 注释中的 @module-tag 应用于该文件中的所有测试,而不仅仅是它前面的那个测试。

考虑这个例子:

describe('forms', () => {
  /**
   * @module-tag frontend
   */
  test('renders a form', () => {
    // ...
  })

  /**
   * @module-tag db
   */
  test('db returns users', () => {
    // ...
  })
})

在这个例子中,文件中的每个测试都将拥有 frontend 和 db 两个标签。若要为单个测试打标签,请改用 options 参数:

describe('forms', () => {
  test('renders a form', { tags: 'frontend' }, () => {
    // ...
  })

  test('db returns users', { tags: 'db' }, () => {
    // ...
  })
})

按标签过滤测试

要仅运行具有特定标签的测试,请使用 --tags-filter CLI 选项:

vitest --tags-filter=frontend
vitest --tags-filter="frontend and backend"

如果你正在运行 Vitest UI,可以使用带 tag: 前缀的过滤器开始通过标签过滤测试,使用相同的标签表达式语法:

如果你使用编程 API,可以将 tagsFilter 选项传递给 startVitest 或 createVitest:

import { startVitest } from 'vitest/node'

await startVitest([], {
  tagsFilter: ['frontend and backend'],
})

或者你可以创建一个带有自定义过滤器的测试规范:

const specification = vitest.getRootProject().createSpecification(
  '/path-to-file.js',
  {
    testTagsFilter: ['frontend and backend'],
  },
)

语法

你可以以不同方式组合标签。Vitest 支持这些关键字:

  • and 或 && 包含两个表达式
  • or 或 || 包含至少一个表达式
  • not 或 ! 排除表达式
  • * 匹配任意数量的字符(0 个或多个)
  • () 对表达式分组并覆盖优先级

解析器遵循标准的运算符优先级:not/! 优先级最高,然后是 and/&&,最后是 or/||。使用括号来覆盖默认优先级。

保留名称

标签名称不能是 and、or 或 not(不区分大小写),因为这些是保留关键字。标签名称也不能包含特殊字符((, ), &, |, !, *, 空格),因为这些字符被表达式解析器使用。

通配符

你可以使用通配符 (*) 来匹配任意数量的字符:

vitest --tags-filter="unit/*"

这将匹配 unit/components、unit/utils 等标签。

排除标签

要排除具有特定标签的测试,在开头添加感叹号 (!) 或 "not" 关键字:

vitest --tags-filter="!slow and not flaky"

示例

以下是一些常见的过滤模式:

# 仅运行单元测试
vitest --tags-filter="unit"

# 运行既是前端又是快速测试
vitest --tags-filter="frontend and fast"

# 运行单元测试或 e2e 测试
vitest --tags-filter="unit or e2e"

# 运行除慢速测试外的所有测试
vitest --tags-filter="!slow"

# 运行非不稳定的前端测试
vitest --tags-filter="frontend && !flaky"

# 运行匹配通配符模式的测试
vitest --tags-filter="api/*"

# 带括号的复杂表达式
vitest --tags-filter="(unit || e2e) && !slow"

# 运行 postgres 或 mysql 的数据库测试,但排除慢速测试
vitest --tags-filter="db && (postgres || mysql) && !slow"

你还可以传递多个 --tags-filter 标志。它们通过 AND 逻辑组合:

# 运行匹配 (unit 或 e2e) 且 非 slow 的测试
vitest --tags-filter="unit || e2e" --tags-filter="!slow"

运行时检查标签过滤器

你可以使用 TestRunner.matchesTags 来检查当前的标签过滤器是否匹配一组标签。这在仅在相关测试被包含时才有条件地运行昂贵设置逻辑时很有用:

import { beforeAll, TestRunner } from 'vitest'

beforeAll(async () => {
  // 当使用 "vitest --tags-filter db" 时播种数据库
  if (TestRunner.matchesTags(['db'])) {
    await seedDatabase()
  }
})

该方法接受一个标签数组,如果当前的 --tags-filter 会包含具有这些标签的测试,则返回 true。如果没有活动的标签过滤器,它总是返回 true。

另请参阅

  • 按文件隔离设置以及并行和串行测试文件使用项目(projects)按文件划分测试。当类别需要不同的运行设置(而非不同的超时或重试)时,请使用项目。

  • 测试过滤涵盖了 -t、--include 和其他 CLI 过滤器。

  • tags 和 strictTags 配置参考。

评论 (0)