单元测试

单元测试是开发应用程序的一个重要(有时被忽略)部分。它们有助于我们的开发流程,确保我们项目的最关键部分不受开发过程中意外错误或疏忽的影响。因此,Vue有自己的测试程序 vue-test-utils。它提供了与 Vue 组件交互的功能,并与许多流行的测试框架一起使用。

Vuetify 使用 Typescript,目前必须导入和继承 Vue 对象。在某些应用程序中,这可能会生成一个 $attrs$listeners 只读警告。目前有一个正在进行的 Github 讨论,为各种测试用例提供潜在的解决方案。如果你还有额外的问题,请加入我们的 在线社区

测试环境设置

关于如何使用 Vue CLI 设置测试运行程序的信息可以在 官方文档 中找到。一目了然,Vue CLI 为以下测试运行程序提供了入门资源库。

引导 Vuetify

为了正确使用 Typescript,Vuetify 组件继承了 Vue 对象。这可能 导致问题 。我们必须在单元测试设置文件中全局安装 Vuetify,而不是使用 localVue instance。这在测试运行程序之间可能有所不同。请务必参考有关安装文件的适当文档。

  1. // test/setup.js
  2. import Vue from 'vue'
  3. import Vuetify from 'vuetify'
  4. Vue.use(Vuetify)

如果您没有使用 setup.js 文件,则应在测试的程序部分中添加 Vue.use(Vuetify)

规范测试

在 Vuetify 中创建类似于 vuexvue-router 的单元测试,因为您将在localVue 实例中使用 Vuetify 对象,并将实例传递给 mount 函数。

让我们创建一个在应用程序中的测试用例示例。

  1. <!-- Vue Component -->
  2. <template>
  3. <v-card>
  4. <v-card-title>
  5. <span v-text="title" />
  6. <v-spacer></v-spacer>
  7. <v-btn @click="$emit('action-btn:clicked')">
  8. Action
  9. </v-btn>
  10. </v-card-title>
  11. <v-card-text>
  12. <slot />
  13. </v-card-text>
  14. </v-card>
  15. </template>
  16. <script>
  17. export default {
  18. props: {
  19. title: {
  20. type: String,
  21. required: true,
  22. },
  23. },
  24. }
  25. </script>

在上面的例子中,我们创建了一个自定义组件 title prop 和 v-btn,当单击它时,它触发一个自定义事件。现在,我们想要创建测试以确保该行为正常工作,并且在将来的更改中也能这样运行。下面的示例是使用 Jest 测试环境创建的。

  1. // test/CustomCard.spec.js
  2. // Libraries
  3. import Vue from 'vue'
  4. import Vuetify from 'vuetify'
  5. // Components
  6. import CustomCard from '@/components/CustomCard'
  7. // Utilities
  8. import {
  9. mount,
  10. createLocalVue
  11. } from '@vue/test-utils'
  12. const localVue = createLocalVue()
  13. describe('CustomCard.vue', () => {
  14. let vuetify
  15. beforeEach(() => {
  16. vuetify = new Vuetify()
  17. })
  18. it('should have a custom title and match snapshot', () => {
  19. const wrapper = mount(CustomCard, {
  20. localVue,
  21. vuetify,
  22. propsData: {
  23. title: 'Foobar',
  24. },
  25. })
  26. // With jest we can create snapshot files of the HTML output
  27. expect(wrapper.html()).toMatchSnapshot()
  28. // We could also verify this differently
  29. // by checking the text content
  30. const title = wrapper.find('.v-card__title > span')
  31. expect(title.text()).toBe('Foobar')
  32. })
  33. it('should emit an event when the action v-btn is clicked', () => {
  34. const wrapper = mount(CustomCard, {
  35. localVue,
  36. vuetify,
  37. propsData: {
  38. title: 'Foobar',
  39. },
  40. })
  41. const event = jest.fn()
  42. const button = wrapper.find('.v-btn')
  43. // Here we bind a listener to the wrapper
  44. // instance to catch our custom event
  45. // https://vuejs.org/v2/api/#Instance-Methods-Events
  46. wrapper.vm.$on('action-btn:clicked', event)
  47. expect(event).toHaveBeenCalledTimes(0)
  48. // Simulate a click on the button
  49. button.trigger('click')
  50. // Ensure that our mock event was called
  51. expect(event).toHaveBeenCalledTimes(1)
  52. })
  53. })

如果你遇到有关测试或者一般问题并且需要帮助,请访问我们的 在线社区

测试效率

在编写测试时,您经常会发现自己在重复同样的事情。在这种情况下,创建助手函数来减少每个测试的重复。DRYing

在单元测试中编写的最常见的重复代码之一是 mount 函数。这可以很容易地编写为可重用函数。

  1. // test/CustomCard.spec.js
  2. describe('CustomCard.vue', () => {
  3. const mountFunction = options => {
  4. return mount(CustomCard, {
  5. localVue,
  6. vuetify,
  7. ...options,
  8. })
  9. }
  10. it('should have a custom title and match snapshot', () => {
  11. const wrapper = mountFunction({
  12. propsData: {
  13. title: 'Fizzbuzz',
  14. },
  15. })
  16. expect(wrapper.html()).toMatchSnapshot()
  17. })
  18. })

Mocking Vuetify

Vuetify 的许多组件都使用全局 $vuetify 对象来生成默认文本或断点信息等设置。在测试这些组件时,需要使用一个模拟对象来提供 vue-test-utils

  1. // test/CustomAlert.spec.js
  2. // Libraries
  3. import Vue from 'vue'
  4. import Vuetify from 'vuetify'
  5. // Components
  6. import CustomAlert from '@/components/CustomAlert'
  7. // Utilities
  8. import {
  9. mount,
  10. createLocalVue
  11. } from '@vue/test-utils'
  12. const localVue = createLocalVue()
  13. describe('CustomAlert.vue', () => {
  14. let vuetify
  15. beforeEach(() => {
  16. vuetify = new Vuetify({
  17. mocks: {
  18. $vuetify: {
  19. lang: {
  20. t: (val: string) => val,
  21. },
  22. },
  23. }
  24. })
  25. })
  26. it('should have a custom title and match snapshot', () => {
  27. const wrapper = mount(CustomAlert, {
  28. localVue,
  29. vuetify,
  30. })
  31. expect(wrapper.html()).toMatchSnapshot()
  32. })
  33. })

请记住,你 只需要存根 正在使用的服务。例如 langapplication。除此之外你也可以手动导入这些服务。

  1. // test/CustomNavigationDrawer.spec.js
  2. // Libraries
  3. import Vue from 'vue'
  4. import Vuetify from 'vuetify'
  5. // Components
  6. import CustomNavigationDrawer from '@/components/CustomNavigationDrawer'
  7. // Utilities
  8. import {
  9. createLocalVue,
  10. mount,
  11. } from '@vue/test-utils'
  12. const localVue = createLocalVue()
  13. describe('CustomNavigationDrawer.vue', () => {
  14. let vuetify
  15. beforeEach(() => {
  16. vuetify = new Vuetify()
  17. })
  18. it('should have a custom title and match snapshot', () => {
  19. const wrapper = mount(CustomNavigationDrawer, {
  20. localVue,
  21. vuetify,
  22. })
  23. expect(wrapper.html()).toMatchSnapshot()
  24. })
  25. })

所有可用服务的完整列表如下:

E2E 测试

Vuetify 将 data-* 属性从组件传递给相关的 HTML 元素,这使得 E2E 测试框架能够轻松地定位它们。

例如,Cypress 建议添加 data-cy 属性,以便更容易地定位元素。

  1. <template>
  2. <!-- Vuetify component with data-cy -->
  3. <v-text-field v-model="name" data-cy="name-input" />
  4. <!-- HTML render output -->
  5. <input data-cy="name-input" id="input-120" type="text">
  6. </template>
  1. // cypress/integration/test.spec.js
  2. describe('Test With Attribute', () => {
  3. it('Find by data-cy', () => {
  4. cy.visit('/')
  5. cy.get('[data-cy=name-input]').type('Zak') // Find element using data-cy
  6. })
  7. })