Skip to content

00|把第一个组件测试跑起来 ​

本页接在第一章的 Vite 实验环境之后。也可以在新空目录运行 npm create vue@3.23.0 vue-test-lab -- --typescript --bare,进入 vue-test-lab 后 npm install。使用 Node 24.15+ 的 24 系列,保留生成的锁文件。本章单元测试运行在 Node 中,用 jsdom 提供 DOM;真实布局、焦点路径和浏览器网络留给第三节 E2E。

安装与目录 ​

在实验项目根安装下列固定版本。它们只属于学习项目:

sh
npm install -D --save-exact vitest@4.1.6 @vue/test-utils@2.5.0 jsdom@27.4.0

Vite 的 Vue 插件由第一章环境提供。测试使用独立配置,避免修改页面构建配置。新建 vitest.config.ts:

ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    include: ['src/**/*.spec.ts'],
    setupFiles: ['./src/test/setup.ts'],
    restoreMocks: true
  }
})

plugins 把 SFC 编译成 JavaScript;environment 让挂载时有 document;include 决定找哪些测试文件。缺少它们时,“无法解析 .vue”“document is not defined”“No test files found”分别发生在不同层,不能靠改组件处理。

新建 src/test/setup.ts:

ts
import { afterEach, vi } from 'vitest'
import { enableAutoUnmount } from '@vue/test-utils'

enableAutoUnmount(afterEach)
afterEach(() => vi.useRealTimers())

自动卸载清理 Vue 组件及其生命周期资源;恢复真实计时器防止一个用例冻结后面的时间。它们不能替应用清理遗漏的监听器,所以仍要测试 onUnmounted 的行为。

从一条能失败的断言开始 ​

新建 src/Counter.vue:

vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template><button @click="count++">{{ count }}</button></template>

新建 src/Counter.spec.ts:

ts
import { mount } from '@vue/test-utils'
import { expect, test } from 'vitest'
import Counter from './Counter.vue'

test('一次点击增加一次', async () => {
  const wrapper = mount(Counter)
  expect(wrapper.get('button').text()).toBe('0')
  await wrapper.get('button').trigger('click')
  expect(wrapper.get('button').text()).toBe('1')
})

运行:

sh
npx vitest run src/Counter.spec.ts

应有一条通过。把组件改成 count += 2 再跑,第二条断言应报告实际 2、期望 1,进程失败;恢复后再继续。这样证明测试确实守住了行为。test 登记用例,expect 在条件不符时抛出失败;mount 返回包装器,get 找不到节点也会直接失败。

等待的对象不同 ​

  • await wrapper.trigger() / setValue():派发事件,再等 Vue 提交本轮更新。
  • await flushPromises():让已能继续的 Promise 链执行;不会自动 resolve 一个永远 pending 的请求。
  • vi.useFakeTimers() + await vi.advanceTimersByTimeAsync(ms):推进受控计时器,也处理过程中派生的异步回调;用完恢复真实时间。

可控请求使用 src/test/deferred.ts:

ts
export function deferred<T>() {
  let resolve!: (value: T) => void
  let reject!: (reason: unknown) => void
  const promise = new Promise<T>((ok, bad) => { resolve = ok; reject = bad })
  return { promise, resolve, reject }
}

先挂载并断言等待态,再手动 resolve/reject,最后 flush。控制的是事件发生顺序,不是猜机器需要多少毫秒。下一节用这个方法验证保存与可取消搜索。

练习:证明清理发生 ​

给 Counter 增加 onUnmounted(() => emit('closed')),在测试里调用 wrapper.unmount(),断言 wrapper.emitted('closed') 恰好只有一项。答案见 环境练习。

用心记录每一步成长