Giter Site home page Giter Site logo

front-end-knowlege's Issues

Vue 项目单元测试

First of All

本文使用的测试工具是jest,关于jest的基本配置与使用,已经有非常优秀的教程
本文不再赘述,All of what you want to know 都在这个教程里。接下来,我们主要介绍利用Vue提供的测试套件工具,对Vue组件进行单元测试。
本文基本环境如下:

  • vue2
  • @vue/composition-api
  • typescript

开始

我们的项目一开始并没有引入测试工具,首先我们需要引入单元测试的工具。我尝试了使用vue add unit-jest的方式,委托vue cli
自动安装单元测试工具,但是因为我们项目环境的复杂性,vue cli并不能识别ts项目,生成的配置项并没有注入到我之前配置的
jest.config.ts文件中,于是我删掉了所有vue cli自动安装的包,选择按照Vue Test Utils
提供的手动安装选项,接下来按部就班的安装就行了。

yarn add -D @vue/test-utils@1 vue-jest

这里我之前对工具函数用jest做了测试,所以我已经对jest做了配置(其实几乎没怎么加配置项)。现在就需要把Vuejest配置手动写进去。

{
  ...
  "moduleFileExtensions": [
    "js",
    "mjs",
    "cjs",
    "jsx",
    "ts",
    "tsx",
    "json",
    "node",
    // 识别vue文件
    "vue"
  ],
  "transform": {
    // 解析ts文件
    "^.+\\.ts?$": 'ts-jest',
    // 解析vue文件
    ".*\\.(vue)$": "vue-jest",
  }
}

Note
这里我们使用的是vue2,所以一定要使用针对vue2的测试套件

yarn add -D @vue/test-utils@1

如果不指定版本安装@vue/test-utils,测试套件就是针对vue3的。

接下来,我们可以在测试目录里添加一个组件测试目录components来测试一下我们的配置有没有问题

<!--test/components/HelloWorld.vue-->
<template>
  <div>Hello World</div>
</template>
// test/components/HelloWorld.test.ts
import {shallowMount} from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld 组件测试', () => {
  it('挂在组件', () => {
    const wrapper = shallowMount(HelloWorld)
    expect(wrapper.text()).toContain('Hello World')
  })
})

测试通过,我们的配置已经生效。接下来,我们可以挂载自己的组件进行单元测试了。

特殊环境的一些“坑”

我们的开发环境前面已经提到了

  • vue2
  • @vue/composition-api
  • typescript

UI组件是我们自研的组件库winbox类似于element-ui。我们在组件中使用了@vue/composition-api,测试中如果我们不注入
这个插件,我们使用setup函数导出的变量是直接进入不了组件实例的,也就是说假设有一个组件

// example/Component.ts
const Component = {
  setup() {
    return {
      test: 1
    }
  }
}
// test/example/Component.test.ts
import {shallowMount} from '@vue/test-utils'
import Component from 'example/Component.ts'
const wrapper = shallowMount(Component)

console.log(wrapper.vm.test === undefined) // expected true

针对vue2 + composition-api 的项目我们需要首先在项目中引入@vue/composition-api
因此上面的test文件改为如下

// test/example/Component.test.ts
import Vue from 'vue'
import CompositionApi from '@vue/composition-api'
import {shallowMount} from '@vue/test-utils'
import Component from 'example/Component.ts'
Vue.use(CompositionApi)
const wrapper = shallowMount(Component)
console.log(wrapper.vm.test === 1) // expected true

这就是在使用@vue/composition-api需要注意的地方。另外,上面的代码属于伪代码,只描述基本逻辑,不保证运行正常。

实际测试

现有待测试组件如下

<!--src/components/warning/RadioOptionGroup.vue-->
<template>
  <section class="option-item">
    <label class="label-name margin"><slot></slot></label>
    <w-radio-group v-model="bindValue" class="options" @change="handleChange">
      <w-radio-button
        v-for="item in options"
        :key="item.label"
        class="btn"
        :label="item.value"
        >{{ item.label }}</w-radio-button
      >
    </w-radio-group>
  </section>
</template>
<script lang="ts">
import { defineComponent, inject, PropType, ref, toRef } from '@vue/composition-api'
export default defineComponent({
  name: 'OptionRadioItem',
  props: {
    options: Array as PropType<{ label: string; value: string | number }[]>,
    binding: String as PropType<string>,
    value: [String, Number] as PropType<string|number>
  },
  model: {
    prop: 'value',
    event: 'change'
  },
  setup (props, { emit }) {
    const key = props.binding || 'default'
    const data = inject('OptionData', { [key]: ref(props.value) })
    const bindValue = toRef(data, key)
    const handleChange = (newVal: string | number) => {
      emit('change', newVal)
    }
    return {
      bindValue,
      handleChange
    }
  }
})
</script>

其中w-radio-groupw-radio-button组件类似于el-radio-groupel-radio-button组件
这个待测试组件要实现的功能基本上和el-radio-group也差不多,接下来我们编写第一个组件测试代码

// test/components/warning/RadioOptionGroup.test.ts
import { mount, shallowMount } from '@vue/test-utils'
import type Vue from 'vue'
import '../../../src/useComposition'
import { provide, reactive } from '@vue/composition-api'

import RadioOptionGroup from '@/components/warning/RadioOptionGroup.vue'
import { RadioGroup, RadioButton } from 'winbox-ui'

type VueEmpower = Vue & {
  bindValue: number | string
  handleChange: (newVal: any) => void
}

describe('单选组件测试', () => {
  // props data
  const options = [
    {
      label: 'name1',
      value: 1
    },
    {
      label: 'name2',
      value: 2
    }
  ]
  // 替换子组件
  const stubs = {
    'w-radio-group': RadioGroup,
    'w-radio-button': RadioButton
  }
  it('作为普通组件展示', () => {
    const wrapper = shallowMount(RadioOptionGroup, {
      propsData: {
        value: 1,
        options: options
      }
    })
    expect(wrapper.text()).toContain('name1')
    expect(wrapper.text()).toContain('name2')
  })

})

执行yarn jest测试通过。这里我们有必要看一下shallowMount挂载后的组件长什么样子

<section class="option-item"><label class="label-name margin"></label>
  <w-radio-group class="options">
    <w-radio-button label="1" class="btn">name1</w-radio-button>
    <w-radio-button label="2" class="btn">name2</w-radio-button>
  </w-radio-group>
</section>

w-radio-groupw-radio-button组件并没有被渲染,即使将shallowMount换成mount 结果并没有改变。
当然这并不影响我们的测试结果的正确性,不过这个正确性是建立在第三方组件没有bug这一前提之上的。
为了让子组件渲染出来,我们需要在挂载组件时挂载配置用到stubs这个选项。
上面的测试中我们在挂载组件使用了挂载配置项,也就是shallowMount传入的第二个参数, (关于配置的选项的使用参考文档)
上面只用到了propsData这个选项,顾名思义就是给组件传入props,而我们要使用到的stubs这个选项,可以当作Vue组件里的components
其实不加stubs选项jest在运行过程中也会报错。
我们将stubs加入到挂载项中,运行之后,就得到了完整的结果。

<section class="option-item"><label class="label-name margin"></label>
  <div role="radiogroup" class="w-radio-group options"><label role="radio" aria-checked="true" tabindex="0" class="w-radio-button btn is-active"><input type="radio" tabindex="-1" autocomplete="off" class="w-radio-button__orig-radio" value="1"><span class="w-radio-button__inner">name1<!----></span></label><label role="radio" tabindex="-1" class="w-radio-button btn"><input type="radio" tabindex="-1" autocomplete="off" class="w-radio-button__orig-radio" value="2"><span class="w-radio-button__inner">name2<!----></span></label></div>
</section>

这时,我们也可以测试组件的交互了,接下来贴的代码只包含it部分的代码,不包含重复代码

it('普通类型组件交互', async () => {
  const wrapper = shallowMount(RadioOptionGroup, {
    propsData: {
      value: 1,
      options: options
    },
    stubs: stubs
  })
  const btns = wrapper.findAllComponents(RadioButton)
  expect(btns.exists()).toBeTruthy()
  await btns.at(0).find('input').trigger('change')
  expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
  await btns.at(1).find('input').trigger('change')
  expect((wrapper.vm as VueEmpower).bindValue).toBe(2)
})

我们还需要测试组件里面的slot是否可以生效

it('组件slot展示', () => {
  const wrapper = shallowMount(RadioOptionGroup, {
    propsData: {
      value: 1,
      options: [
        {
          label: 'name1',
          value: 1
        },
        {
          label: 'name2',
          value: 2
        }
      ]
    },
    slots: {
      default: 'This is a title'
    }
  })
  expect(wrapper.text()).toContain('This is a title')
})

我们的组件中使用了provide由父组件提供数据,因此也需要对这一功能进行测试。但由于我们使用的是@vue/composition-api
提供的provide函数,当我直接配置挂载项里的provide项时并没有生效,在这一部分我选择了构造一个parentComponent
为测试组件提供数据,同时也需要在挂载项中提供parentComponent配置项。

it('provide 类型组件交互', async () => {
  const wrapper = shallowMount(RadioOptionGroup, {
    propsData: {
      options: [
        {
          label: 'name1',
          value: 1
        },
        {
          label: 'name2',
          value: 2
        }
      ],
      binding: 'test'
    },
    stubs: stubs,
    parentComponent: {
      setup () {
        provide('OptionData', { test: 1 })
      }
    }
  })

  expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
  const btns = wrapper.findAllComponents(RadioButton)
  expect(btns.exists()).toBeTruthy()
  await btns.at(0).find('input').trigger('change')
  expect((wrapper.vm as VueEmpower).bindValue).toBe(1)
  await btns.at(1).find('input').trigger('change')
  expect((wrapper.vm as VueEmpower).bindValue).toBe(2)
})

这时测试组件里面的inject逻辑就生效了,不过这并不完美。接下来我们需要测试组件的change事件是否正常,在这一部分其实有
陷阱。在上面的provide中,我们提供的是一个普通的对象,而且也通过了测试。然而如果只是把上面的代码
稍加改造检查handleChange函数的调用情况,那么我们可能会失望。

it('change 事件测试', async () => {
    const wrapper = shallowMount(RadioOptionGroup, {
      propsData: {
        options: [
          {
            label: 'name1',
            value: 1
          },
          {
            label: 'name2',
            value: 2
          }
        ],
        binding: 'test'
      },
      stubs: stubs,
      parentComponent: {
        setup () {
          provide('OptionData', {test: 2})
        }
      }
    })

    const vm = wrapper.vm as VueEmpower
    const spy = jest.spyOn(vm, '$emit')
    const spy2 = jest.spyOn(vm, 'handleChange')
    expect(spy).toBeCalledTimes(0)
    const btns = wrapper.findAllComponents(RadioButton)
    await btns.at(0).find('input').trigger('change')
    expect(vm.bindValue).toBe(1)
    expect(spy2).toBeCalledTimes(1)
    expect(spy2.mock.lastCall[0]).toBe(1)
    expect(spy.mock.lastCall).toEqual(['change', 1])
  })

在这段测试代码中,总是卡在expect(spy2).toBeCalledTimes(1)这行代码,经过一系列实验之后,我最终发现
父组件的provide提供的对象不是响应式的,导致handleChange函数不被调用。其实道理也很好理解,
vdom的渲染变化依赖响应式对象的变化,这里我们使用provide对象并不是响应式的,所以对象的变化
并不会触发vdom的重新渲染,也就不会触发change事件。这里还有一个知识点,provide函数(配置项)并不会使普通对象
变成一个响应式对象,为了让handleChange事件可以正常触发,我们需要把provide里面的对象改成响应式的。

it('change 事件测试', async () => {
    const wrapper = shallowMount(RadioOptionGroup, {
      propsData: {
        options: [
          {
            label: 'name1',
            value: 1
          },
          {
            label: 'name2',
            value: 2
          }
        ],
        binding: 'test'
      },
      stubs: stubs,
      parentComponent: {
        setup () {
          provide('OptionData', reactive({test: 2}))
        }
      }
    })

    const vm = wrapper.vm as VueEmpower
    const spy = jest.spyOn(vm, '$emit')
    const spy2 = jest.spyOn(vm, 'handleChange')
    expect(spy).toBeCalledTimes(0)
    const btns = wrapper.findAllComponents(RadioButton)
    await btns.at(0).find('input').trigger('change')
    expect(vm.bindValue).toBe(1)
    expect(spy2).toBeCalledTimes(1)
    expect(spy2.mock.lastCall[0]).toBe(1)
    expect(spy.mock.lastCall).toEqual(['change', 1])
  })

所有对象全部通过,我们完成了一个Vue Component组件的测试

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.