一、什么是单元测试?
这次聚焦在单元测试如何验证每个独立功能。要区分单元测试和覆盖率测试的关系,强调单元测试是如何测试每一个步骤是否都正确的手段,覆盖率是衡量指标。
单元测试 就是不让小机器人一次性做完整个蛋糕,而是:
- 隔离 :把它关进一个透明的、没有任何其他干扰的迷你厨房(测试环境)。
- 专注 :每次只让它严格遵循食谱中的一个步骤(一个单元),并给它准备好的材料(输入)。
- 验证 :然后你检查它这一步的产出是否符合你的预期(断言)。
单元测试就是针对程序中最小的可测试单元(通常是函数或模块)进行正确性检验的工作。
二、Vue 2 单元测试的运行机制(流程图)
官方 2.6 分支 不用 Jest ,也 不用 Vue Test Utils ;所有单元用例都跑 Karma + Mocha + PhantomJS (或 ChromeHeadless),源码位于 test/unit/
。
Vue 2 的单元测试核心是 "隔离" 和 "模拟" 。我们不启动整个应用,而是单独实例化一个 Vue 组件或调用一个函数,并模拟它所需的一切依赖(如 DOM、外部模块)。
以下是 Vue 2 单元测试的完整流程:

- 准备阶段
- 工具链
-- Karma:测试运行器
-- Mocha:测试框架(describe / it
)
-- PhantomJS / ChromeHeadless:无头浏览器容器
-- Sinon-Chai:提供 spy / stub / expect 语法 - 启动方式
npm run test:unit
→ Karma 读取test/unit/karma.conf.js
,把src/
和test/unit/specs/
打包后丢进浏览器跑。
- 执行与断言阶段
- 挂载组件
直接new Vue(options).$mount()
到真实 DOM(<div id="app"></div>
)。 - 模拟交互
手动dispatchEvent
或wrapper.$el.querySelector('button').click()
。 - 断言
用expect(vm.someData).to.equal(...)
(Chai BDD 语法)。 - 典型文件
test/unit/specs/vdom/create-element.spec.js
、component.spec.js
等。
- 拆解阶段
- Karma 在每次用例结束后自动刷新 iframe,Mocha 负责清理全局状态,保证用例隔离。
三、简单示例:测试一个计数器组件
假设我们有一个简单的 Vue 2 计数器组件。
源码 (src/components/Counter.vue
)
xml
<template>
<div>
<span class="count">{{ count }}</span>
<button @click="increment">点我+1</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
}
}
}
</script>
单元测试 (tests/unit/Counter.spec.js
)
我们使用 Jest + Vue Test Utils 来为这个组件编写单元测试。
javascript
// 1. 引入必要的依赖
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';
// 2. 使用 `describe` 定义一个测试套件,描述你要测试什么
describe('Counter.vue', () => {
// 3. 使用 `it` 定义一个具体的测试用例
it('渲染初始计数为 0', () => {
// Arrange: 挂载组件
const wrapper = shallowMount(Counter);
// Assert: 断言组件中的文本包含 0
// .text() 获取组件文本内容
// .toContain() 是 Jest 的匹配器,用于判断是否包含
expect(wrapper.text()).toContain('0');
});
it('点击按钮后计数增加 1', async () => {
// Arrange: 挂载组件
const wrapper = shallowMount(Counter);
// Act: 模拟用户点击按钮
// .find() 查找 button 元素
// .trigger() 触发一个 'click' 事件
await wrapper.find('button').trigger('click');
// Assert: 断言计数现在为 1
// 注意:这里我们检查的是 data 里的 count,
// 但更好的做法是检查 DOM 是否更新,因为用户看到的是 DOM
expect(wrapper.vm.count).toBe(1); // 检查数据
expect(wrapper.text()).toContain('1'); // 检查 DOM 更新,这样更好!
});
});
运行测试 (package.json
脚本)
通常在 package.json
中会配置一个脚本:
json
{
"scripts": {
"test:unit": "jest"
}
}
然后在终端运行 npm run test:unit
。Jest 会找到所有 *.spec.js
文件并执行。
输出结果
如果一切正常,你会看到:
scss
PASS tests/unit/Counter.spec.js
Counter.vue
✓ 渲染初始计数为 0 (5ms)
✓ 点击按钮后计数增加 1 (3ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
如果测试失败(比如你把 increment
方法写错了),Jest 会给出非常详细的错误信息,告诉你预期是什么,实际得到的是什么,帮你快速定位问题。
四、Vue 2 源码自身的单元测试
Vue 2 源码本身的单元测试也是这个原理,只不过测试的不是业务组件,而是框架的核心部分:
-
测试响应式系统(Reactivity)
-
示例:创建一个响应式对象,修改它的属性,然后断言依赖它的"侦听函数"被正确调用了。
-
代码片段(概念性) :
scssit('should trigger watcher when dependency changes', () => { const obj = reactive({ count: 0 }); const watcherFn = jest.fn(); // 一个模拟的侦听函数 // 激活依赖收集,让 obj.count 和 watcherFn 建立关联 watch(watcherFn, () => obj.count); obj.count++; // Act: 修改依赖 expect(watcherFn).toHaveBeenCalled(); // Assert: 侦听函数被调用 });
-
-
测试编译器(Compiler)
- 示例 :给编译器一段模板字符串(如
'<div>{{ msg }}</div>'
),断言它编译后生成的渲染函数代码是否正确。
- 示例 :给编译器一段模板字符串(如
-
测试虚拟DOM(Virtual DOM)
- 示例:断言两个虚拟DOM节点进行 Diff 算法后,产生的补丁(patches)是否正确。
总结:单元测试 vs. 覆盖率测试
特性 | 单元测试 (Unit Test) | 覆盖率测试 (Coverage Test) |
---|---|---|
目的 | 验证代码逻辑是否正确 | 衡量测试用例是否充分 |
焦点 | 质量(Quality) | 数量(Quantity) |
关系 | 因:我们编写测试用例 | 果:由单元测试的执行结果计算得出 |
比喻 | 检查食谱每一步的结果对不对 | 检查食谱有多少步骤被做过了 |
结论 :
你为 Vue 2 组件或源码编写的单元测试 ,是产生覆盖率报告 的前提和原料。你先要有很多好的、覆盖不同场景的单元测试,然后利用覆盖率工具(如 Istanbul)去分析这些测试到底执行了源码的哪些部分,从而发现测试的盲区。