vue-36(为组件编写单元测试:属性、事件和方法)
为组件编写单元测试:属性、事件和方法
测试 Vue 组件对于确保其可靠性和可维护性至关重要。通过编写单元测试,我们可以隔离组件并验证它们在不同场景下的行为是否符合预期。本课程重点介绍测试组件的属性、事件和方法,这些是 Vue 组件功能的基本方面。
配置测试环境
在深入测试特定组件功能之前,让我们确保您的测试环境已正确配置。正如上一课中提到的,我们将使用 Jest 作为测试运行器,并使用 Vue Test Utils 来与测试中的 Vue 组件进行交互。
如果您尚未安装,请在项目中安装这些依赖项:
npm install --save-dev @vue/test-utils jest
通过在项目根目录创建一个 jest.config.js
文件来配置 Jest:
module.exports = {moduleFileExtensions: ['js','jsx','json','vue'],transform: {'^.+\\.vue$': 'vue-jest','.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub','^.+\\.jsx?$': 'babel-jest'},transformIgnorePatterns: ['/node_modules/'],moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1'},snapshotSerializers: ['jest-serializer-vue'],testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],testURL: 'http://localhost/'
}
此配置告诉 Jest 如何处理 Vue 组件和其他文件类型。它还设置了 Vue 组件的模块别名和快照序列化器。
测试组件属性
属性用于从父组件传递数据到子组件。测试属性涉及验证组件是否正确接收并渲染属性值。
基础属性测试
让我们考虑一个简单的 Greeting
组件,它接受一个 name
属性:
// src/components/Greeting.vue
<template><div><h1>Hello, {{ name }}!</h1></div>
</template><script>
export default {props: {name: {type: String,required: true}}
}
</script>
要测试这个组件,我们可以使用 Vue Test Utils 来挂载组件并传入一个 prop 值:
// tests/unit/Greeting.spec.js
import { shallowMount } from '@vue/test-utils';
import Greeting from '@/components/Greeting.vue';describe('Greeting.vue', () => {it('renders the greeting with the provided name', () => {const name = 'John Doe';const wrapper = shallowMount(Greeting, {propsData: { name }});expect(wrapper.text()).toContain(`Hello, ${name}!`);});
});
在这个测试中:
- 我们从 Vue Test Utils 中导入
shallowMount
和Greeting
组件。 - 我们使用
it
定义一个测试用例。 - 我们定义一个
name
变量,其值为我们要作为 prop 传递的值。 - 我们使用
shallowMount
创建Greeting
组件的浅层包装器,通过propsData
选项传递name
prop。 - 我们使用
expect
和toContain
来断言组件渲染的文本包含预期的问候消息。
测试属性验证
Vue 允许你为 props 定义验证规则,例如指定数据类型或要求必须存在某个 prop。让我们为我们的 Greeting
组件添加一个类型验证:
// src/components/Greeting.vue
<template><div><h1>Hello, {{ name }}!</h1></div>
</template><script>
export default {props: {name: {type: String,required: true}}
}
</script>
现在,我们来编写一个测试,以确保如果 name
属性不是字符串,组件会抛出错误:
// tests/unit/Greeting.spec.js
import { shallowMount } from '@vue/test-utils';
import Greeting from '@/components/Greeting.vue';describe('Greeting.vue', () => {it('renders the greeting with the provided name', () => {const name = 'John Doe';const wrapper = shallowMount(Greeting, {propsData: { name }});expect(wrapper.text()).toContain(`Hello, ${name}!`);});it('throws an error if the name prop is not a string', () => {const consoleErrorSpy = jest.spyOn(console, 'error');shallowMount(Greeting, {propsData: { name: 123 }});expect(consoleErrorSpy).toHaveBeenCalled();consoleErrorSpy.mockRestore();});
});
在这个测试中:
- 我们使用
jest.spyOn(console, 'error')
来监视console.error
方法,该方法在属性验证错误发生时被调用。 - 我们使用无效的
name
属性(数字而非字符串)挂载Greeting
组件。 - 我们使用
expect(consoleErrorSpy).toHaveBeenCalled()
来断言console.error
方法被调用,表明发生了属性验证错误。 - 我们使用
consoleErrorSpy.mockRestore()
在测试后恢复原始的console.error
方法。
测试默认属性值
你也可以为属性定义默认值。让我们为我们的 Greeting
组件添加一个默认值:
// src/components/Greeting.vue
<template><div><h1>Hello, {{ name }}!</h1></div>
</template><script>
export default {props: {name: {type: String,default: 'Guest'}}
}
</script>
现在,我们来编写一个测试,以确保如果未提供 name
属性,组件会渲染默认问候语:
// tests/unit/Greeting.spec.js
import { shallowMount } from '@vue/test-utils';
import Greeting from '@/components/Greeting.vue';describe('Greeting.vue', () => {it('renders the greeting with the provided name', () => {const name = 'John Doe';const wrapper = shallowMount(Greeting, {propsData: { name }});expect(wrapper.text()).toContain(`Hello, ${name}!`);});it('renders the default greeting if no name prop is provided', () => {const wrapper = shallowMount(Greeting);expect(wrapper.text()).toContain('Hello, Guest!');});
});
在这个测试中,我们挂载了 Greeting
组件,但没有提供 name
属性。然后我们断言渲染的文本包含默认的问候消息。
测试组件事件
组件可以向其父组件发出自定义事件进行通信。测试事件涉及验证组件在特定操作发生时是否发出正确的事件以及预期的有效载荷。
触发简单事件
让我们考虑一个当被点击时会触发 Button
事件的组件:click
// src/components/Button.vue
<template><button @click="handleClick">Click me</button>
</template><script>
export default {methods: {handleClick() {this.$emit('click');}}
}
</script>
要测试这个组件,我们可以使用 Vue Test Utils 来模拟一个点击事件,并验证 click
事件是否被触发:
// tests/unit/Button.spec.js
import { shallowMount } from '@vue/test-utils';
import Button from '@/components/Button.vue';describe('Button.vue', () => {it('emits a click event when clicked', () => {const wrapper = shallowMount(Button);wrapper.find('button').trigger('click');expect(wrapper.emitted().click).toBeTruthy();});
});
在这个测试中:
- 我们从 Vue Test Utils 中导入
shallowMount
,以及Button
组件。 - 我们挂载
Button
组件。 - 我们使用
wrapper.find('button')
在组件中查找按钮元素。 - 我们使用
trigger('click')
模拟按钮的点击事件。 - 我们使用
wrapper.emitted()
来获取组件发出的事件。 - 我们使用
expect(wrapper.emitted().click).toBeTruthy()
来断言click
事件已被发出。
发送带有有效负载的事件
组件也可以发出带有有效载荷的事件,有效载荷是与事件一起传递的数据值。让我们修改我们的 Button
组件,使其发出一个 click
事件,并将当前时间戳作为有效载荷:
// src/components/Button.vue
<template><button @click="handleClick">Click me</button>
</template><script>
export default {methods: {handleClick() {const timestamp = Date.now();this.$emit('click', timestamp);}}
}
</script>
现在,我们来编写一个测试用例,以验证 click
事件是否以正确的时戳负载被触发:
// tests/unit/Button.spec.js
import { shallowMount } from '@vue/test-utils';
import Button from '@/components/Button.vue';describe('Button.vue', () => {it('emits a click event when clicked', () => {const wrapper = shallowMount(Button);wrapper.find('button').trigger('click');expect(wrapper.emitted().click).toBeTruthy();});it('emits a click event with the current timestamp as the payload', () => {const wrapper = shallowMount(Button);wrapper.find('button').trigger('click');const emittedClick = wrapper.emitted().click;expect(emittedClick[0][0]).toBeGreaterThan(0); // Check if the timestamp is a positive number});
});
在这个测试中:
- 我们使用
click
事件通过wrapper.emitted().click
来检索。 - 我们通过
emittedClick[0][0]
访问事件的负载。emittedClick
是一个数组,包含多个数组。外层数组包含事件被触发的所有时间点,内层数组包含传递给$emit
调用的参数。 - 我们断言负载是一个大于0的数字,这表明它是一个有效的时间戳。
测试事件处理器
有时,你可能需要测试在事件触发时是否调用了特定方法。例如,假设我们的 Button
组件有一个名为 logClick
的方法,当按钮被点击时会向控制台记录一条消息:
// src/components/Button.vue
<template><button @click="handleClick">Click me</button>
</template><script>
export default {methods: {handleClick() {this.$emit('click');this.logClick();},logClick() {console.log('Button clicked!');}}
}
</script>
要测试这一点,我们可以模拟 logClick
方法,并验证当按钮被点击时它是否被调用:
// tests/unit/Button.spec.js
import { shallowMount } from '@vue/test-utils';
import Button from '@/components/Button.vue';describe('Button.vue', () => {it('emits a click event when clicked', () => {const wrapper = shallowMount(Button);wrapper.find('button').trigger('click');expect(wrapper.emitted().click).toBeTruthy();});it('calls the logClick method when clicked', () => {const wrapper = shallowMount(Button);const logClickSpy = jest.spyOn(wrapper.vm, 'logClick');wrapper.find('button').trigger('click');expect(logClickSpy).toHaveBeenCalled();logClickSpy.mockRestore();});
});
在这个测试中:
- 我们使用
jest.spyOn(wrapper.vm, 'logClick')
来监视组件实例logClick
方法(wrapper.vm
)。 - 我们在按钮上模拟点击事件。
- 我们使用
expect(logClickSpy).toHaveBeenCalled()
断言logClick
方法被调用。 - 我们使用
logClickSpy.mockRestore()
在测试后恢复原始的logClick
方法。
测试组件方法
组件通常包含执行特定任务的方法。测试方法涉及验证它们在调用时返回正确的值或产生预期的副作用。
测试一种简单方法
让我们考虑一个具有名为 Counter
的方法的 increment
组件,该方法用于增加计数器值:
// src/components/Counter.vue
<template><div><p>Count: {{ count }}</p><button @click="increment">Increment</button></div>
</template><script>
export default {data() {return {count: 0};},methods: {increment() {this.count++;}}
}
</script>
要测试这个组件,我们可以使用 Vue Test Utils 来访问组件实例并直接调用 increment
方法:
// tests/unit/Counter.spec.js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';describe('Counter.vue', () => {it('increments the count when the increment method is called', () => {const wrapper = shallowMount(Counter);const vm = wrapper.vm; // Access the Vue instancevm.increment();expect(vm.count).toBe(1);});
});
在这个测试中:
- 我们挂载了
Counter
组件。 - 我们使用
wrapper.vm
访问组件实例。 - 我们直接使用
increment
方法,通过vm.increment()
调用。 - 我们断言,
count
数据属性已被增加到 1。
测试带参数的方法
方法也可以接受参数。让我们修改我们的 Counter
组件,添加一个 incrementBy
方法,该方法可以将计数器增加指定数量:
// src/components/Counter.vue
<template><div><p>Count: {{ count }}</p><button @click="increment">Increment</button></div>
</template><script>
export default {data() {return {count: 0};},methods: {incrementBy(amount) {this.count += amount;}}
}
</script>
现在,我们来编写一个测试用例,以验证 incrementBy
方法是否正确地将计数器增加指定的数量:
// tests/unit/Counter.spec.js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';describe('Counter.vue', () => {it('increments the count when the increment method is called', () => {const wrapper = shallowMount(Counter);const vm = wrapper.vm; // Access the Vue instancevm.incrementBy(5);expect(vm.count).toBe(5);});
});
在这个测试中,我们调用 incrementBy
方法,传入参数 5,并断言 count
数据属性已经增加到 5。
测试异步方法
有时,方法可能会执行异步操作,例如进行 API 调用。测试异步方法需要处理代码的异步特性。
让我们考虑一个使用 async/await
从 API 获取数据的组件:
// src/components/DataFetcher.vue
<template><div><p v-if="loading">Loading...</p><p v-else-if="error">Error: {{ error }}</p><p v-else>Data: {{ data }}</p></div>
</template><script>
import axios from 'axios';export default {data() {return {data: null,loading: false,error: null};},methods: {async fetchData() {this.loading = true;this.error = null;try {const response = await axios.get('/api/data');this.data = response.data;} catch (error) {this.error = error.message;} finally {this.loading = false;}}},mounted() {this.fetchData();}
}
</script>
要测试这个组件,我们可以使用 jest.mock
来模拟 axios
库,并控制 API 调用的响应:
// tests/unit/DataFetcher.spec.js
import { shallowMount } from '@vue/test-utils';
import DataFetcher from '@/components/DataFetcher.vue';
import axios from 'axios';jest.mock('axios');describe('DataFetcher.vue', () => {it('fetches data from the API and updates the data property', async () => {const mockData = { message: 'Hello, world!' };axios.get.mockResolvedValue({ data: mockData });const wrapper = shallowMount(DataFetcher);await wrapper.vm.$nextTick(); // Wait for the component to updateexpect(wrapper.vm.data).toEqual(mockData);expect(wrapper.vm.loading).toBe(false);expect(wrapper.vm.error).toBe(null);});it('handles errors when fetching data from the API', async () => {const errorMessage = 'Request failed with status code 404';axios.get.mockRejectedValue(new Error(errorMessage));const wrapper = shallowMount(DataFetcher);await wrapper.vm.$nextTick(); // Wait for the component to updateexpect(wrapper.vm.data).toBe(null);expect(wrapper.vm.loading).toBe(false);expect(wrapper.vm.error).toBe(errorMessage);});
});
在这些测试中:
- 我们使用
jest.mock('axios')
来模拟axios
库。 - 我们使用
axios.get.mockResolvedValue
来模拟一个带有模拟数据的成功 API 响应。 - 我们使用
axios.get.mockRejectedValue
来模拟一个带有错误信息的失败 API 响应。 - 我们挂载了
DataFetcher
组件。 - 我们使用
await wrapper.vm.$nextTick()
来等待异步操作完成后组件的更新。 - 我们断言,
data
、loading
和error
数据属性已经根据 API 响应正确更新。