介绍
TDD
TDD 测试驱动开发,基于代码测试用例进行开发,能够确保代码质量,并能够反向驱动代码的开发,一步步推进。可以大大减少开发中,反复修复bug的时候。
vitest
vitest很好的集成到vue3中,用法与jest类似。
搭建
安装
npm create vue@latest
# 选择 ts(是) -> jsx(是) -> vue router(是) -> pinia(是) -> vitest (是) -> -> e2e (否) -> eslint(否) -> Vue DevTools(否)
✔ 请输入项目名称: … myvitest
✔ 是否使用 TypeScript 语法? … 否 / 是(是)
✔ 是否启用 JSX 支持? … 否 / 是(是)
✔ 是否引入 Vue Router 进行单页面应用开发? … 否 / 是(是)
✔ 是否引入 Pinia 用于状态管理? … 否 / 是(是)
✔ 是否引入 Vitest 用于单元测试? … 否 / 是(是)
✔ 是否要引入一款端到端(End to End)测试工具? › 不需要(否)
✔ 是否引入 ESLint 用于代码质量检测? … 否 / 是(否)
✔ 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) … 否 / 是(否)
# 安装
npm i
代码结构
配置介绍
vitest有自己专门的config vitest.config.ts
这里定义了支持vue语法和dom的一些库
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
},
});
用例一般用__test__文件夹命名
生成的用例代码
src/components/HelloWorld.vue
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
...
</style>
src/components/tests/HelloWorld.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})
该用例测试测试传入的msg,是否在挂载后渲染出来
测试用例用describe包裹,用it包裹测试用例,用expect包裹期望值,用toBe包裹期望值
运行
npm run test:unit
注意
用create vue@latest生成的代码, vitest的版本使用的是^1.6.0,,在运行npm run test:unit会有bug提示
修复办法1
- 在node执行前先执行
unset NODE_OPTIONS清空变量,再执行npm run test:unit - 或者在package.json里面的script
"test:unit": "unset NODE_OPTIONS && vitest"
修复办法2
修改vitest版本和配置
修改package.json,把 "vitest": "^1.6.0" 改成 => "vitest": "latest" 使用最新版
并且修改
tsconfig.app.json 删除 "extends": "@vue/tsconfig/tsconfig.dom.json",
tsconfig.node.json 删除 "extends": "@tsconfig/node20/tsconfig.json",
记得删除package-lock.json和node_modules再重新执行。npm i安装
然后再运行
nice~
进阶
介绍纯js和vue组件相关的测试逻辑。新增的代码结构如下
常用方法
describe:用于分组测试,可以描述一个测试组的行为。it/test:定义单个测试用例。expect:用于断言测试结果。beforeEach/afterEach:在每个测试之前/之后运行的钩子。beforeAll/afterAll:在所有测试之前/之后运行的钩子。
纯js的用例
常用法
import { describe, it, expect } from 'vitest';
describe('Array', () => {
it('should start empty', () => {
const arr = [];
expect(arr).toHaveLength(0);
});
it('should add elements correctly', () => {
const arr = [];
arr.push(1);
expect(arr).toHaveLength(1);
expect(arr[0]).toBe(1);
});
});
异步使用
import { describe, it, expect } from 'vitest';
describe('Async function', () => {
it('should resolve', async () => {
const asyncFunc = () => new Promise((resolve) => setTimeout(() => resolve('done'), 100));
const result = await asyncFunc();
expect(result).toBe('done');
});
it('should reject', async () => {
const asyncFunc = () => new Promise((_, reject) => setTimeout(() => reject(new Error('error')), 100));
await expect(asyncFunc()).rejects.toThrow('error');
});
});
各种钩子逻辑
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
let globalCounter = 0;
beforeAll(() => {
// 在所有测试之前运行
console.log('beforeAll');
});
afterAll(() => {
// 在所有测试之后运行
console.log('afterAll');
});
describe('Counter', () => {
beforeEach(() => {
// 在每个测试之前运行
globalCounter = 0;
});
afterEach(() => {
// 在每个测试之后运行
console.log('afterEach');
});
it('should be 0 initially', () => {
expect(globalCounter).toBe(0);
});
it('should increment', () => {
globalCounter++;
expect(globalCounter).toBe(1);
});
});
vue组件例子
计数组件 src/components/Counter.vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'Counter',
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
return {
count,
increment,
decrement
};
}
});
</script>
Counter 对应用例 src/components/Counter.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from '../Counter.vue';
describe('Counter.vue', () => {
it('renders initial count', () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('0');
});
it('increments count when Increment button is clicked', async () => {
const wrapper = mount(Counter);
const button = wrapper.find('button:first-of-type');
await button.trigger('click');
expect(wrapper.text()).toContain('1');
});
it('decrements count when Decrement button is clicked', async () => {
const wrapper = mount(Counter);
const incrementButton = wrapper.find('button:first-of-type');
const decrementButton = wrapper.find('button:last-of-type');
await incrementButton.trigger('click');
await decrementButton.trigger('click');
expect(wrapper.text()).toContain('0');
});
it('increments and decrements count', async () => {
const wrapper = mount(Counter);
const vm = wrapper.vm as any;
// 检查初始状态
expect(vm.count).toBe(0);
// 调用方法并检查状态变化
vm.increment();
await wrapper.vm.$nextTick();
expect(vm.count).toBe(1);
vm.decrement();
await wrapper.vm.$nextTick();
expect(vm.count).toBe(0);
});
});
SlotComponent插槽测试src/components/SlotComponent.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'SlotComponent'
});
</script>
SlotComponent用例 src/components/SlotComponent.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import SlotComponent from '../SlotComponent.vue';
describe('SlotComponent.vue', () => {
it('renders slot content', () => {
const wrapper = mount(SlotComponent, {
slots: {
default: '<p>Hello, Slot!</p>'
}
});
expect(wrapper.html()).toContain('<p>Hello, Slot!</p>');
});
});
AsyncComponent.vue测试异步组件 src/components/AsyncComponent.vue
<!-- -->
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else>{{ data }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: 'AsyncComponent',
setup() {
const loading = ref(true);
const data = ref('');
onMounted(async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
data.value = result.data;
loading.value = false;
});
return {
loading,
data
};
}
});
</script>
AsyncComponent.vue测试用例 src/components/AsyncComponent.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi } from 'vitest';
import AsyncComponent from '../AsyncComponent.vue';
// 模拟 fetch API
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'Hello, Async!' }),
})
);
describe('AsyncComponent.vue', () => {
it('renders loading state initially', () => {
const wrapper = mount(AsyncComponent);
expect(wrapper.text()).toContain('Loading...');
});
it('renders data after fetch', async () => {
const wrapper = mount(AsyncComponent);
await new Promise(setImmediate); // 等待异步任务完成
await wrapper.vm.$nextTick(); // 等待 Vue 组件更新
expect(wrapper.text()).toContain('Hello, Async!');
});
});