当前位置:   article > 正文

Vue 组件单元测试深度探索:细致解析与实战范例大全_vue 单元测试

vue 单元测试

在这里插入图片描述
Vue.js作为一款广受欢迎的前端框架,以其声明式的数据绑定、组件化开发和灵活的生态系统赢得了广大开发者的心。然而,随着项目规模的增长,确保组件的稳定性和可靠性变得愈发关键。单元测试作为软件质量的守护神,为Vue组件的开发过程提供了坚实的质量保障。本文旨在深入探讨Vue组件单元测试的理论基础、最佳实践以及丰富的实例演示,为前端开发者打造一套全面且实用的Vue组件测试宝典。

本文全面的介绍了 Vue 组件单元测试应该涵盖主要方面:

  1. 组件挂载与渲染
  2. Props 接收与响应
  3. 数据模型(Data)
  4. 计算属性(Computed)
  5. 方法(Methods)
  6. 生命周期钩子
  7. 事件监听与触发
  8. 条件与循环渲染
  9. 模板指令(如 v-if, v-for, v-model 等)
  10. 组件交互与状态变更
  11. 依赖注入
  12. Vuex Store
  13. 国际化(i18n)与主题支持(如果有)

在对 Vue 组件进行单元测试时,确保组件正确挂载并进行渲染是基础且关键的步骤。这涉及使用测试框架(如 Jest + Vue Test Utils)创建测试环境,然后挂载组件并检查其渲染输出。以下是对这一过程的详细解释和举例:

创建测试环境

请查看《Jest测试框架全方位指南:从安装,preset、transform、testMatch等jest.config.js配置,多模式测试命令到测试目录规划等最佳实践》

一、组件挂载与渲染

1. 挂载组件

使用 Vue Test Utils 提供的 shallowMountmount 函数来挂载组件。两者的主要区别在于:

  • shallowMount 只渲染组件本身及其直接子组件,不渲染孙子组件及更深层级的组件,有助于隔离测试目标组件。
  • mount 完全渲染组件及其所有嵌套组件,适用于需要测试组件间交互或依赖全局插件/指令的情况。
import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      // 可以传递 props、slots、mocks、scopedSlots、attachToDocument 等选项
      propsData: { title: 'Test Title' },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  // 测试用例...
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2. 渲染输出检查

挂载后,可以对组件的渲染输出进行各种检查,以确保其结构、内容、样式等符合预期。

组件渲染

模板快照测试:使用Jest的toMatchSnapshot方法,确保组件的初始渲染结果与期望一致。

it('renders correctly', () => {
  expect(wrapper.html()).toMatchSnapshot();
});
  • 1
  • 2
  • 3

检查 HTML 结构

使用 wrapper.html() 获取组件渲染后的完整 HTML 字符串,然后进行字符串匹配检查:

it('renders expected HTML structure', () => {
  expect(wrapper.html()).toContain('<div class="my-component">');
  expect(wrapper.html()).toContain('<h1>Test Title</h1>');
});
  • 1
  • 2
  • 3
  • 4

查询 DOM 元素

使用 wrapper.find()wrapper.findAll()wrapper.query()wrapper.queryAll() 等方法查询 DOM 元素,根据需要进行存在性、数量、属性、类名等检查:

it('contains specific elements', () => {
  const header = wrapper.find('h1');
  expect(header.exists()).toBe(true);
  expect(header.text()).toBe('Test Title');

  const buttons = wrapper.findAll('button');
  expect(buttons.length).toBe(2);
});

it('applies correct classes', () => {
  const myDiv = wrapper.find('.my-component');
  expect(myDiv.classes()).toContain('is-active');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

模拟事件

触发组件上的事件,并检查组件状态或外部行为(如 emit 的自定义事件)是否随之改变:

it('handles button click', async () => {
  const button = wrapper.find('button');
  button.trigger('click');

  await wrapper.vm.$nextTick(); // 确保异步更新完成

  expect(wrapper.emitted().customEvent).toBeTruthy();
  expect(wrapper.emitted().customEvent[0]).toEqual(['some-data']);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

通过以上步骤,可以对 Vue 组件进行完整的挂载与渲染测试,涵盖结构、内容、交互等多个方面。实际编写测试时,应根据组件的实际功能和复杂度,选择合适的测试点和断言,确保覆盖关键逻辑和可能的边缘情况。同时,遵循良好的测试实践,如保持测试独立、避免过度耦合、提高测试的可读性和可维护性。

二、Props接收与响应

在对Vue组件进行单元测试时,验证组件正确接收Props并对其做出响应至关重要。以下是对这一方面的详细解释与举例:

1. 定义Props并传递给组件

在测试组件之前,确保组件已定义所需的Props。在组件文件(如 MyComponent.vue)中,使用 props 选项声明Props:

<script>
export default {
  props: {
    title: {
      type: String,
      required: true,
    },
    items: {
      type: Array,
      default: () => [],
    },
    isEnabled: {
      type: Boolean,
      default: false,
    },
  },
  // ...
};
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2. 在测试中传递Props

使用Vue Test Utils的shallowMountmount函数挂载组件时,可以通过propsData选项传递Props:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      propsData: {
        title: 'Test Title',
        items: [{ id: 1, name: 'Item 1' }],
        isEnabled: true,
      },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  // 测试用例...
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3. 检查Props接收与响应

3.1. 检查组件内部Props值

确认组件接收到Props后,其内部状态(通常是组件实例的props属性)应与传递的值一致:

it('receives props correctly', () => {
  expect(wrapper.props().title).toBe('Test Title');
  expect(wrapper.props().items).toEqual([{ id: 1, name: 'Item 1' }]);
  expect(wrapper.props().isEnabled).toBe(true);
});
  • 1
  • 2
  • 3
  • 4
  • 5

3.2. 检查Props影响的DOM结构

验证Props值是否正确地影响了组件的渲染输出。例如,检查带有条件渲染的元素是否根据Prop值显示或隐藏:

it('renders based on prop "isEnabled"', () => {
  expect(wrapper.find('.content').exists()).toBe(true); // 假设'.content'元素依赖于'isEnabled'

  wrapper.setProps({ isEnabled: false });
  expect(wrapper.find('.content').exists()).toBe(false);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.3. 检查Props引发的方法调用或状态变化

测试当Props更改时,组件是否正确响应,如触发特定方法、更新内部状态或发出事件:

it('triggers a method when prop "items" changes', async () => {
  const updateItemsSpy = jest.spyOn(wrapper.vm, 'updateInternalState');

  wrapper.setProps({ items: [{ id: 2, name: 'Item 2' }] });
  await wrapper.vm.$nextTick(); // 确保异步更新完成

  expect(updateItemsSpy).toHaveBeenCalled();
});

it('emits an event when prop "title" changes', async () => {
  wrapper.setProps({ title: 'New Title' });
  await wrapper.vm.$nextTick();

  expect(wrapper.emitted().titleChanged).toBeTruthy();
  expect(wrapper.emitted().titleChanged[0]).toEqual(['New Title']);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.4. 覆盖多种Prop值情况

为了确保组件对各种可能的Prop值都有正确的响应,编写测试用例覆盖边界条件、默认值、异常值等:

it('handles empty array for "items"', () => {
  wrapper.setProps({ items: [] });
  expect(wrapper.find('.no-items-message').exists()).toBe(true);
});

it('displays fallback title when "title" is not provided', () => {
  wrapper.setProps({ title: undefined });
  expect(wrapper.text()).toContain('Fallback Title');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

通过上述步骤,可以全面测试Vue组件对Props的接收与响应,包括检查内部Props值、DOM结构变化、方法调用与状态更新等。确保覆盖各种Prop值情况,以验证组件在不同输入下的行为是否符合预期。遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

三、数据模型(Data)

在Vue组件单元测试中,验证组件的数据模型(Data)正确初始化、更新和响应变化至关重要。以下是对这一方面的详细解释与举例:

1. 定义组件数据模型

在组件文件(如 MyComponent.vue)中,使用 data 选项定义数据模型:

<script>
export default {
  data() {
    return {
      internalValue: '',
      items: [],
      isLoading: false,
      errorMessage: '',
    };
  },
  // ...
};
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2. 检查数据模型初始值

测试组件实例化后,其内部数据模型应具有预期的初始值:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent);
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('initializes data correctly', () => {
    expect(wrapper.vm.internalValue).toBe('');
    expect(wrapper.vm.items).toEqual([]);
    expect(wrapper.vm.isLoading).toBe(false);
    expect(wrapper.vm.errorMessage).toBe('');
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

3. 测试数据模型更新

3.1. 触发数据更新的方法

测试组件中负责更新数据的方法,确认它们能正确修改数据模型:

it('updates data via method', async () => {
  wrapper.vm.updateInternalValue('New Value');
  expect(wrapper.vm.internalValue).toBe('New Value');

  wrapper.vm.addItem({ id: 1, name: 'Item 1' });
  expect(wrapper.vm.items).toEqual([{ id: 1, name: 'Item 1' }]);
});

it('sets "isLoading" to true when loading data', async () => {
  wrapper.vm.startLoading();
  expect(wrapper.vm.isLoading).toBe(true);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.2. 响应式数据变化的DOM更新

验证数据模型变化后,组件视图是否相应更新:

it('reflects data changes in the DOM', async () => {
  wrapper.vm.internalValue = 'Updated Value';
  await wrapper.vm.$nextTick(); // 确保DOM更新完成

  expect(wrapper.find('input').element.value).toBe('Updated Value');

  wrapper.setData({ errorMessage: 'An error occurred' });
  await wrapper.vm.$nextTick();

  expect(wrapper.text()).toContain('An error occurred');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3.3. 数据变化引发的副作用

测试数据变化时,组件是否触发了预期的副作用,如事件发射、API请求等:

it('emits an event when data changes', async () => {
  wrapper.setData({ internalValue: 'Changed Value' });
  await wrapper.vm.$nextTick();

  expect(wrapper.emitted().valueChanged).toBeTruthy();
  expect(wrapper.emitted().valueChanged[0]).toEqual(['Changed Value']);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.4. 覆盖多种数据状态情况

编写测试用例覆盖数据模型的各种状态,包括边界条件、异常值、空值等:

it('handles empty array for "items"', () => {
  wrapper.setData({ items: [] });
  expect(wrapper.find('.no-items-message').exists()).toBe(true);
});

it('displays error message when "errorMessage" is set', () => {
  wrapper.setData({ errorMessage: 'An error occurred' });
  expect(wrapper.text()).toContain('An error occurred');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

通过上述步骤,可以全面测试Vue组件的数据模型,包括数据初始化、更新逻辑、DOM响应以及引发的副作用。确保覆盖各种数据状态情况,以验证组件在不同数据状态下的行为是否符合预期。遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

四、计算属性(Computed)

在Vue组件单元测试中,验证计算属性(Computed)的正确计算、响应式更新以及对视图的影响至关重要。以下是对这一方面的详细解释与举例:

1. 定义计算属性

在组件文件(如 MyComponent.vue)中,使用 computed 选项定义计算属性:

<script>
export default {
  props: {
    basePrice: {
      type: Number,
      required: true,
    },
    taxRate: {
      type: Number,
      default: 0.1,
    },
  },
  data() {
    return {
      quantity: 1,
    };
  },
  computed: {
    totalPrice() {
      return this.basePrice * (1 + this.taxRate) * this.quantity;
    },
  },
  // ...
};
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

2. 检查计算属性值

测试计算属性是否根据其依赖关系正确计算值:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      propsData: {
        basePrice: 100,
      },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('computes totalPrice correctly', () => {
    expect(wrapper.vm.totalPrice).toBe(110); // 默认税率为10%

    wrapper.setProps({ taxRate: 0.2 });
    expect(wrapper.vm.totalPrice).toBe(120); // 更新税率后总价应变更为120

    wrapper.setData({ quantity: 2 });
    expect(wrapper.vm.totalPrice).toBe(240); // 更新数量后总价应变更为240
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

3. 测试计算属性的响应式更新

验证计算属性的值在依赖数据变化时能够自动更新,并且视图也随之更新:

it('updates totalPrice reactively when dependencies change', async () => {
  wrapper.setProps({ taxRate: 0.2 });
  await wrapper.vm.$nextTick(); // 确保DOM更新完成

  expect(wrapper.find('.total-price').text()).toBe('Total Price: $120.00');

  wrapper.setData({ quantity: 2 });
  await wrapper.vm.$nextTick();

  expect(wrapper.find('.total-price').text()).toBe('Total Price: $240.00');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4. 覆盖多种计算结果情况

编写测试用例覆盖计算属性的各种计算结果,包括边界条件、异常值、依赖关系变化等:

it('handles zero basePrice', () => {
  wrapper.setProps({ basePrice: 0 });
  expect(wrapper.vm.totalPrice).toBe(0);
});

it('displays an error when taxRate exceeds 1', async () => {
  wrapper.setProps({ taxRate: 1.1 });
  await wrapper.vm.$nextTick();

  expect(wrapper.find('.error-message').exists()).toBe(true);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5. 测试计算属性的缓存机制

Vue的计算属性具有缓存机制,当依赖数据未发生改变时,多次访问计算属性将返回相同的值而无需重新计算。测试此特性:

it('caches computed value when dependencies do not change', () => {
  const spy = jest.spyOn(wrapper.vm, 'totalPriceGetter'); // 假设totalPrice定义为get函数

  // 第一次访问
  const firstResult = wrapper.vm.totalPrice;
  expect(spy).toHaveBeenCalledTimes(1);

  // 第二次访问,依赖数据未变,应从缓存中获取
  const secondResult = wrapper.vm.totalPrice;
  expect(secondResult).toBe(firstResult);
  expect(spy).toHaveBeenCalledTimes(1); // 仅被调用一次,表明从缓存中获取

  // 更改依赖数据,再次访问
  wrapper.setData({ quantity: 2 });
  const thirdResult = wrapper.vm.totalPrice;
  expect(thirdResult).not.toBe(firstResult); // 值已改变
  expect(spy).toHaveBeenCalledTimes(2); // 被调用两次,因为依赖数据变了
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

总结

通过上述步骤,可以全面测试Vue组件的计算属性,包括计算逻辑、响应式更新、视图同步以及缓存机制。确保覆盖各种计算结果情况,以验证组件在不同计算属性状态下的行为是否符合预期。遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

五、方法(Methods)

在Vue组件单元测试中,验证组件内定义的方法(Methods)的逻辑正确性、副作用触发以及与其他组件属性(如数据、计算属性、Props等)的交互至关重要。以下是对这一方面的详细解释与举例:

1. 定义组件方法

在组件文件(如 MyComponent.vue)中,使用 methods 选项定义方法:

<script>
export default {
  // ...
  methods: {
    addItem(item) {
      this.items.push(item);
      this.$emit('item-added', item);
    },
    updateQuantity(newQuantity) {
      if (newQuantity < 1 || newQuantity > 100) {
        this.showError('Invalid quantity');
        return;
      }
      this.quantity = newQuantity;
    },
    showError(message) {
      this.errorMessage = message;
      this.$nextTick(() => {
        setTimeout(() => {
          this.errorMessage = '';
        }, 3000);
      });
    },
  },
  // ...
};
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

2. 测试方法逻辑

验证方法执行后,其内部逻辑是否正确,包括状态变更、条件判断、循环、递归等:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      propsData: {
        basePrice: 100,
      },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('adds an item to the list and emits event', () => {
    const newItem = { id: 1, name: 'Item 1' };

    wrapper.vm.addItem(newItem);

    expect(wrapper.vm.items).toContainEqual(newItem);
    expect(wrapper.emitted().itemAdded).toBeTruthy();
    expect(wrapper.emitted().itemAdded[0]).toEqual([newItem]);
  });

  it('updates quantity within valid range', () => {
    wrapper.vm.updateQuantity(50);
    expect(wrapper.vm.quantity).toBe(50);

    wrapper.vm.updateQuantity(99);
    expect(wrapper.vm.quantity).toBe(99);
  });

  it('handles invalid quantity and displays error message', () => {
    wrapper.vm.updateQuantity(0);
    expect(wrapper.vm.errorMessage).toBe('Invalid quantity');

    wrapper.vm.updateQuantity(101);
    expect(wrapper.vm.errorMessage).toBe('Invalid quantity');
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

3. 测试方法副作用

验证方法执行时是否触发了预期的副作用,如事件发射、状态更新、API请求等:

it('clears error message after a timeout', async () => {
  wrapper.vm.showError('Test Error');
  await wrapper.vm.$nextTick(); // 更新errorMessage

  expect(wrapper.vm.errorMessage).toBe('Test Error');

  jest.runAllTimers(); // 快速推进所有定时器

  await wrapper.vm.$nextTick(); // 确保errorMessage清除后DOM更新完成

  expect(wrapper.vm.errorMessage).toBe('');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4. 覆盖多种方法执行情况

编写测试用例覆盖方法的各种执行情况,包括边界条件、异常值、依赖关系变化等:

it('handles empty items array', () => {
  wrapper.setData({ items: [] });
  wrapper.vm.addItem({ id: 1, name: 'Item 1' });

  expect(wrapper.vm.items).toEqual([{ id: 1, name: 'Item 1' }]);
});

it('prevents updating quantity below zero', () => {
  wrapper.vm.updateQuantity(-1);
  expect(wrapper.vm.quantity).toBe(1);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5. 模拟依赖的方法或服务

如果方法内部依赖其他方法、外部服务(如API请求)等,可以使用 jest.fn()jest.mock() 创建模拟函数或模块,以控制其返回值或行为:

import axios from 'axios';

jest.mock('axios', () => ({
  post: jest.fn(() => Promise.resolve({ data: { success: true } })),
}));

// ...

it('makes a POST request to add an item', async () => {
  await wrapper.vm.addItem({ id: 1, name: 'Item 1' });

  expect(axios.post).toHaveBeenCalledWith('/api/items', { id: 1, name: 'Item 1' });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

总结

通过上述步骤,可以全面测试Vue组件的方法,包括逻辑正确性、副作用触发、与其他组件属性的交互以及模拟依赖的方法或服务。确保覆盖各种方法执行情况,以验证组件在不同方法调用状态下的行为是否符合预期。遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

六、生命周期钩子

在Vue组件单元测试中,验证生命周期钩子函数的正确执行以及它们对组件状态的影响至关重要。以下是对生命周期钩子的详细解释与举例:

1. Vue组件生命周期概述

Vue组件在其生命周期中有多个关键阶段,每个阶段对应一个或多个生命周期钩子函数。这些钩子提供了在特定时刻执行代码的机会,以便管理组件的创建、更新、销毁等过程。主要生命周期阶段包括:

  • 创建阶段

    • beforeCreate: 在实例初始化之后、数据观测和事件配置之前被调用。
    • created: 实例已经创建完成,数据观测、属性和方法的运算、watch/event回调已完成初始化,但尚未挂载到DOM中。
  • 挂载阶段

    • beforeMount: 在挂载开始之前被调用。
    • mounted: 实例被新创建的$el替换,并挂载到DOM中。
  • 更新阶段

    • beforeUpdate: 数据发生变化但尚未更新DOM时被调用。
    • updated: 数据更新导致DOM重新渲染后被调用。
  • 销毁阶段

    • beforeDestroy: 在实例销毁之前调用,此时实例仍然完全可用。
    • destroyed: 实例已经被销毁,所有绑定关系解除,事件监听器移除,子实例也已销毁。

此外,还有与keep-alive相关的activateddeactivated钩子,以及只在服务器端渲染(SSR)中使用的serverPrefetch钩子。

2. 测试生命周期钩子

2.1. 验证钩子函数执行

通过 spies(间谍函数)或 mock(模拟函数)来检查特定生命周期钩子是否在预期的时机被调用:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;
  let createdSpy;
  let mountedSpy;
  let updatedSpy;
  let destroyedSpy;

  beforeEach(() => {
    createdSpy = jest.fn();
    mountedSpy = jest.fn();
    updatedSpy = jest.fn();
    destroyedSpy = jest.fn();

    MyComponent.created = createdSpy;
    MyComponent.mounted = mountedSpy;
    MyComponent.updated = updatedSpy;
    MyComponent.destroyed = destroyedSpy;

    wrapper = shallowMount(MyComponent);
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('calls lifecycle hooks', () => {
    expect(createdSpy).toHaveBeenCalled();
    expect(mountedSpy).toHaveBeenCalled();

    // 触发数据更新以触发updated钩子
    wrapper.setData({ value: 'new value' });
    await wrapper.vm.$nextTick();
    expect(updatedSpy).toHaveBeenCalled();

    wrapper.destroy();
    expect(destroyedSpy).toHaveBeenCalled();
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

2.2. 测试钩子函数内部逻辑

若钩子函数内包含复杂的逻辑,如异步操作、API调用、DOM操作等,应针对这些逻辑编写单独的测试:

it('fetches data in created hook', async () => {
  // 假设组件在created钩子中调用了`fetchData()`方法
  const fetchDataSpy = jest.spyOn(wrapper.vm, 'fetchData');

  // 模拟fetchData返回值
  wrapper.vm.fetchData.mockResolvedValueOnce({ data: 'mocked data' });

  await wrapper.vm.$nextTick(); // 确保created钩子执行完成

  expect(fetchDataSpy).toHaveBeenCalled();
  expect(wrapper.vm.data).toEqual('mocked data');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.3. 测试钩子引发的副作用

检查生命周期钩子是否触发了预期的副作用,如状态变更、事件发射、外部资源加载等:

it('emits event in mounted hook', async () => {
  // 假设组件在mounted钩子中发射了一个名为'component-mounted'的事件
  await wrapper.vm.$nextTick(); // 确保mounted钩子执行完成

  expect(wrapper.emitted().componentMounted).toBeTruthy();
});

it('cleans up resources in beforeDestroy hook', () => {
  const cleanupResourcesSpy = jest.spyOn(wrapper.vm, 'cleanupResources');

  wrapper.destroy();

  expect(cleanupResourcesSpy).toHaveBeenCalled();
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3. 覆盖多种生命周期场景

编写测试用例覆盖组件在不同生命周期阶段的多种场景,如首次创建、数据更新、组件重用(对于keep-alive组件)、组件销毁等:

it('handles keep-alive activation', async () => {
  // 假设组件在activated钩子中执行某些逻辑
  const activatedSpy = jest.spyOn(wrapper.vm, 'onActivated');

  // 模拟组件进入inactive状态,然后重新激活
  wrapper.vm.deactivate();
  await wrapper.vm.$nextTick();
  wrapper.vm.activate();

  expect(activatedSpy).toHaveBeenCalled();
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

总结

通过上述步骤,可以全面测试Vue组件的生命周期钩子,包括钩子函数的执行、内部逻辑、引发的副作用以及覆盖多种生命周期场景。确保测试覆盖组件在生命周期各个阶段的关键行为,遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。注意,实际测试时应根据组件的具体实现调整测试策略。

七、事件监听与触发

在Vue组件单元测试中,验证组件内事件监听器的设置、事件触发后的响应逻辑以及事件传播行为是不可或缺的部分。以下是对这一方面的详细解释与举例:

1. Vue组件事件概述

Vue组件支持自定义事件,通过 this.$emit 发送事件,并通过 v-on@ 语法在模板中监听组件上的事件。组件还可以使用 $on, $off, $once 等方法动态监听和管理事件。

2. 测试事件监听器

2.1. 验证事件监听器设置

确保组件正确设置了事件监听器,通常可以通过检查组件实例上的 $on 方法调用来实现:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent);
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('registers event listeners on mount', () => {
    const spy = jest.spyOn(wrapper.vm, '$on');

    expect(spy).toHaveBeenCalledWith('customEvent', expect.any(Function));
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2.2. 测试事件触发后的响应逻辑

触发组件上的自定义事件,然后检查组件状态、DOM更新或其他预期行为是否正确发生:

it('responds to customEvent', async () => {
  wrapper.vm.$emit('customEvent', { someData: 'test data' });

  await wrapper.vm.$nextTick(); // 确保DOM更新完成

  expect(wrapper.vm.internalState).toEqual({ ...expectedStateAfterEvent });
  expect(wrapper.find('.event-response').text()).toContain('Event Handled');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.3. 测试事件传播(冒泡/捕获)

验证事件是否按照预期进行冒泡或捕获,以及父组件是否正确处理子组件传递的事件:

const parentWrapper = shallowMount(ParentComponent, {
  slots: {
    default: `<my-component></my-component>`,
  },
});

it('bubbles customEvent from child to parent', async () => {
  const childWrapper = parentWrapper.findComponent(MyComponent);

  childWrapper.vm.$emit('customEvent');

  await parentWrapper.vm.$nextTick();

  expect(parentWrapper.emitted().childCustomEvent).toBeTruthy();
});

it('captures customEvent from child to parent', async () => {
  const childWrapper = parentWrapper.findComponent(MyComponent);

  parentWrapper.vm.$on('customEvent', parentEventHandler);

  childWrapper.trigger('customEvent');

  await parentWrapper.vm.$nextTick();

  expect(parentEventHandler).toHaveBeenCalled();
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3. 测试动态事件监听与移除

对于使用 $on, $off, $once 动态管理事件的组件,需验证这些方法的调用是否正确:

it('removes event listener on demand', () => {
  const handlerSpy = jest.fn();

  wrapper.vm.$on('customEvent', handlerSpy);
  wrapper.vm.$emit('customEvent');

  expect(handlerSpy).toHaveBeenCalled();

  wrapper.vm.$off('customEvent', handlerSpy);
  wrapper.vm.$emit('customEvent');

  expect(handlerSpy).toHaveBeenCalledTimes(1); // 仅在注册期间被调用一次
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

4. 覆盖多种事件触发场景

编写测试用例覆盖组件在不同场景下对事件的响应,如不同事件类型、携带不同参数的事件、在特定状态下的事件处理等:

it('handles customEvent with different payload types', async () => {
  wrapper.vm.$emit('customEvent', { type: 'string', value: 'test' });
  await wrapper.vm.$nextTick();
  expect(wrapper.vm.processedPayload).toBe('Processed: test');

  wrapper.vm.$emit('customEvent', { type: 'number', value: 42 });
  await wrapper.vm.$nextTick();
  expect(wrapper.vm.processedPayload).toBe('Processed: 42');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

总结

通过上述步骤,可以全面测试Vue组件的事件监听与触发功能,包括事件监听器设置、事件触发后的响应逻辑、事件传播行为以及动态事件管理。确保测试覆盖组件在不同事件场景下的行为,遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

八、事件监听与触发

在Vue组件单元测试中,验证条件渲染(如 v-ifv-elsev-show)和循环渲染(如 v-for)的功能正确性及其对组件结构和状态的影响至关重要。以下是对这一方面的详细解释与举例:

1. 条件渲染测试

1.1. 验证条件分支展示与隐藏

检查条件分支在不同数据状态下是否正确显示或隐藏:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      propsData: { condition: false },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('renders conditional content based on prop', async () => {
    // 验证初始状态
    expect(wrapper.find('.when-condition-false').exists()).toBe(true);
    expect(wrapper.find('.when-condition-true').exists()).toBe(false);

    // 更新条件并验证变化
    wrapper.setProps({ condition: true });
    await wrapper.vm.$nextTick();

    expect(wrapper.find('.when-condition-false').exists()).toBe(false);
    expect(wrapper.find('.when-condition-true').exists()).toBe(true);
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

1.2. 测试条件分支内部逻辑

如果条件分支内部包含复杂逻辑,如方法调用、计算属性依赖等,应针对这些逻辑编写单独的测试:

it('executes method inside v-if block when condition is true', () => {
  wrapper.setProps({ condition: true });

  const myMethodSpy = jest.spyOn(wrapper.vm, 'myMethod');

  wrapper.vm.$nextTick();

  expect(myMethodSpy).toHaveBeenCalled();
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2. 循环渲染测试

2.1. 验证列表元素生成与更新

检查组件是否正确根据数据列表生成对应的DOM元素,并在列表更新时同步更新视图:

it('renders list items based on array', () => {
  wrapper.setProps({
    items: ['Item 1', 'Item 2', 'Item 3'],
  });

  const listItems = wrapper.findAll('.list-item');

  expect(listItems.length).toBe(3);
  expect(listItems.at(0).text()).toContain('Item 1');
  expect(listItems.at(1).text()).toContain('Item 2');
  expect(listItems.at(2).text()).toContain('Item 3');

  // 更新列表并验证变化
  wrapper.setProps({
    items: ['Updated Item 1', 'Updated Item 2'],
  });
  await wrapper.vm.$nextTick();

  const updatedListItems = wrapper.findAll('.list-item');
  expect(updatedListItems.length).toBe(2);
  expect(updatedListItems.at(0).text()).toContain('Updated Item 1');
  expect(updatedListItems.at(1).text()).toContain('Updated Item 2');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.2. 测试循环内部逻辑与事件

如果循环体内部包含复杂逻辑、事件监听等,应针对这些内容编写单独的测试:

it('binds click event to each list item', async () => {
  wrapper.setProps({
    items: ['Item 1', 'Item 2'],
  });

  const listItemEls = wrapper.findAll('.list-item');
  const clickSpy = jest.spyOn(wrapper.vm, 'handleItemClick');

  listItemEls.at(0).trigger('click');
  await wrapper.vm.$nextTick();
  expect(clickSpy).toHaveBeenCalledWith('Item 1');

  listItemEls.at(1).trigger('click');
  await wrapper.vm.$nextTick();
  expect(clickSpy).toHaveBeenCalledWith('Item 2');
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.3. 测试 key 属性的作用

验证使用 key 属性时,Vue在更新列表时能正确保留和移动已有元素,避免不必要的DOM重排:

it('preserves DOM elements using keys during list updates', async () => {
  wrapper.setProps({
    items: [{ id: 1, text: 'Item 1' }, { id: 2, text: 'Item 2' }],
  });

  const initialElements = wrapper.findAll('.list-item');
  const firstElementInitialTextContent = initialElements.at(0).element.textContent;

  wrapper.setProps({
    items: [{ id: 2, text: 'Updated Item 2' }, { id: 1, text: 'Item 1' }],
  });
  await wrapper.vm.$nextTick();

  const updatedElements = wrapper.findAll('.list-item');
  const firstElementUpdatedTextContent = updatedElements.at(0).element.textContent;

  // 验证元素内容是否按预期交换位置,而非重新创建
  expect(firstElementUpdatedTextContent).toBe(firstElementInitialTextContent);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3. 覆盖多种条件与循环场景

编写测试用例覆盖组件在不同条件与循环场景下的行为,如空列表、列表增删、条件切换等:

it('handles empty list', () => {
  wrapper.setProps({ items: [] });
  expect(wrapper.findAll('.list-item').length).toBe(0);
});

it('appends new items to the list', async () => {
  wrapper.setProps({ items: ['Item 1'] });
  await wrapper.vm.$nextTick();

  wrapper.setProps({ items: ['Item 1', 'Item 2'] });
  await wrapper.vm.$nextTick();

  expect(wrapper.findAll('.list-item').length).toBe(2);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

总结

通过上述步骤,可以全面测试Vue组件的条件与循环渲染功能,包括条件分支展示与隐藏、条件分支内部逻辑、列表元素生成与更新、循环内部逻辑与事件、key 属性的作用等。确保测试覆盖组件在不同条件与循环场景下的行为,遵循良好的测试实践,编写清晰、独立且易于维护的测试用例。

九、模板指令(如 v-if, v-for, v-model 等)

在Vue组件单元测试中,验证模板指令(如 v-bindv-modelv-onv-slot 等)的正确性和功能完整性对于确保组件的正常工作至关重要。以下是对这些指令的测试详解与举例:

1. v-bind (动态绑定)

1.1. 验证属性值绑定

测试组件是否根据数据属性正确设置了元素的HTML属性:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyComponent, {
      propsData: { myProp: 'value' },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('binds property value dynamically', () => {
    const element = wrapper.find('.my-element');

    expect(element.attributes('title')).toBe('value');
    expect(element.attributes('href')).toContain('value');

    // 更新属性值并验证变化
    wrapper.setProps({ myProp: 'updatedValue' });
    await wrapper.vm.$nextTick();

    expect(element.attributes('title')).toBe('updatedValue');
    expect(element.attributes('href')).toContain('updatedValue');
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

1.2. 测试对象式绑定

对于使用 v-bind 绑定的对象(如 v-bind="{ attr: value }"),确保组件能够正确响应对象属性的变化:

it('binds object properties dynamically', () => {
  wrapper.setData({ bindingObject: { title: 'initialTitle', disabled: false } });

  const element = wrapper.find('.my-element');

  expect(element.attributes('title')).toBe('initialTitle');
  expect(element.element.disabled).toBe(false);

  // 更新对象属性并验证变化
  wrapper.setData({ bindingObject: { title: 'newTitle', disabled: true } });
  await wrapper.vm.$nextTick();

  expect(element.attributes('title')).toBe('newTitle');
  expect(element.element.disabled).toBe(true);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2. v-model

测试组件是否正确实现了双向数据绑定,包括输入值与数据属性间的同步更新:

import { shallowMount } from '@vue/test-utils';
import MyFormComponent from '@/components/MyFormComponent.vue';

describe('MyFormComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MyFormComponent, {
      propsData: { initialValue: 'initialValue' },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('implements two-way data binding with v-model', async () => {
    const inputElement = wrapper.find('input[type="text"]');

    // 初始值验证
    expect(inputElement.element.value).toBe('initialValue');
    expect(wrapper.vm.formValue).toBe('initialValue');

    // 更新输入值并验证数据属性变化
    inputElement.setValue('newValue');
    await wrapper.vm.$nextTick();

    expect(inputElement.element.value).toBe('newValue');
    expect(wrapper.vm.formValue).toBe('newValue');

    // 更新数据属性并验证输入值变化
    wrapper.setData({ formValue: 'updatedValue' });
    await wrapper.vm.$nextTick();

    expect(inputElement.element.value).toBe('updatedValue');
    expect(wrapper.vm.formValue).toBe('updatedValue');
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

3. v-on

测试组件是否正确响应事件绑定,包括自定义事件和原生DOM事件:

import { shallowMount } from '@vue/test-utils';
import MyClickableComponent from '@/components/MyClickableComponent.vue';

describe('MyClickableComponent', () => {
  let wrapper;
  let clickHandlerSpy;

  beforeEach(() => {
    clickHandlerSpy = jest.fn();
    wrapper = shallowMount(MyClickableComponent, {
      listeners: { click: clickHandlerSpy },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('triggers click event handler', async () => {
    wrapper.find('.clickable-element').trigger('click');
    await wrapper.vm.$nextTick();

    expect(clickHandlerSpy).toHaveBeenCalled();
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

4. v-slot

测试组件是否正确处理作用域插槽内容及传递的插槽props:

import { shallowMount } from '@vue/test-utils';
import MySlotComponent from '@/components/MySlotComponent.vue';

describe('MySlotComponent', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(MySlotComponent, {
      slots: {
        default: '<div class="slot-content" v-bind="slotProps"></div>',
      },
      scopedSlots: {
        customSlot: '<div class="custom-slot-content">{{ slotProps.text }}</div>',
      },
    });
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('renders default slot content with bound props', async () => {
    wrapper.setData({ slotProps: { text: 'Default Slot Text' } });
    await wrapper.vm.$nextTick();

    const defaultSlot = wrapper.find('.slot-content');
    expect(defaultSlot.text()).toBe('Default Slot Text');
  });

  it('renders custom scoped slot with passed props', async () => {
    wrapper.setData({ slotProps: { text: 'Custom Slot Text' } });
    await wrapper.vm.$nextTick();

    const customSlot = wrapper.find('.custom-slot-content');
    expect(customSlot.text()).toBe('Custom Slot Text');
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

总结

通过以上示例,我们可以看到如何针对Vue组件中的各种模板指令编写单元测试,确保它们在实际应用中正确执行动态绑定、双向数据绑定、事件处理以及作用域插槽功能。在编写测试时,关注指令与数据属性、事件、插槽内容之间的交互,确保组件在各种场景下都能正确响应和更新。

十、组件交互与状态变更

请查看《Vue 组件单元测试深度探索:组件交互与状态变更 专业解析和实践》

十二、Vuex Store

请查看《Vuex Store全方位指南:从目录结构、模块化设计,到Modules、state、mutations、actions、getters核心概念最佳实践及全面测试策略》

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/797190
推荐阅读
相关标签
  

闽ICP备14008679号