Vue - 单元测试

191 阅读3分钟

基础概念

单元测试是针对程序的最小单元进行正确性检验的测试工作。这个可测试的最小单元指的是某个方法,某个 api。

为什么需要单元测试

单元测试是伴随项目的扩展和工程化演进的必然结果。伴随着项目的扩大,通过简单的功能黑盒测试,可能会出现无法覆盖整个项目的情况。而且当某个功能发生了变化,需要进行的回归测试也会是一个工作量很大并且不确定性很高的环节。因此,在大的项目中,逐渐加入单元测试是很有必要的。

有了单元测试的过程的加入,开发方式也出现了一些新的变化,在现在的一些大厂中也已经在应用,即测试驱动开发。测试驱动开发有分为 2 中方式:TDD,BDD。

TDD: 根据客户需求编写测试用例,对功能的过程和接口都进行了设计,而且这种从使用者角度对代码进行的设计通常更符合后期开发的需求。侧重于从软件功能进行测试用例的编写。

BDD: BDD采用更容易测试的软件需求描述方式鼓励需求分析人员、软件开发人员、测试人员密切协同开展软件产品研发工作。侧重将软件需求通过工具转化为自动化测试的脚本。这里常用的工具有:cucumber。

怎么编写单元测试

因为 jest 包含了 karma + mocha + chai + sinon 的所有常用功能,具备了断言、JSDom、覆盖率报告等能力,零配置开箱即用,所以这里我选择 jest 的测试框架。

第一步:使用 vue-cli4 快速搭建工程,选择 jest 作为单元测试;

第二步:增加产生测试报告的配置,在 jest.cofig.js 中增加配置:

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'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  snapshotSerializers: ['jest-serializer-vue'],
  testMatch: [
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
  coverageDirectory: '<rootDir>/tests/unit/coverage',
  'collectCoverage': true,
  'coverageReporters': [
    'lcov',
    'text-summary'
  ],
  testURL: 'http://localhost/'
}

第三步:在 test/unit 目录下创建文件夹 components 文件夹,并创建 counter.spec.js ,该文件是针对 src/components 文件夹下的 Counter 组件测测试文件。

Counter 组件代码:

<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">Increment</button>
    <hello-world @custom="onCustom"></hello-world>
    <p v-if="beEmitted">Emitted!</p>
  </div>
</template>

<script>
import HelloWorld from "./HelloWorld.vue";
export default {
  components: {
    HelloWorld
  },
  data() {
    return {
      count: 0,
      beEmitted: false
    };
  },

  methods: {
    increment() {
      this.count++;
    },
    onCustom() {
      this.beEmitted = true
    }
  }
};
</script>

counter.spec.js

其中这几个测试内容的编写是使用 Vue 推荐的工具 vue-test-utils 中的例子。

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

const wrapper = shallowMount (counter)
const counterVm = wrapper.vm

describe('counter.vue', () => {
    it ('renders the correct markup', () => {
        expect(wrapper.html()).toContain('<span class="count">0</span>')
    })

    it ('if contains a button', () => {
        expect(wrapper.contains('button')).toBe(true)
    })

    it ('模拟用户交互', () => {
        expect(counterVm.count).toBe(0)
        const button = wrapper.find('button')
        button.trigger('click');
        expect(counterVm.count).toBe(1)
    })

    // 该部分目前遇到问题,测试失败,暂为找到原因
    it ('模拟子组件触发事件测试', () => {
        expect(counterVm.beEmitted).toBe(false)
        wrapper.find(HelloWorld).vm.$emit('custom')
        expect(counterVm.beEmitted).toBe(true)
        expect(wrapper.html()).toContain('Emitted!')
    })
})

在这里的测试子组件触发事件的测试中,出现的组件中的有渲染条件的标签无法测试通过,即通过直接调用方法改变 beEmitted = true,或者通过子组件 $emit 来触发都未测试通过。

<p v-if="beEmitted">Emitted!</p> // 根据 beEmitted 的值决定是否渲染该元素 

后面考虑是否是由于渲染的原因导致,增加 nextTick 后,测时通过。

    it('模拟子组件触发事件测试', done => {
        const wrapper = shallowMount(counter)
        wrapper.find(HelloWorld).vm.$emit('custom')
        Vue.nextTick(() => {
            expect(wrapper.html()).toContain('Emitted')
            done()
        })

这里也验证了该 vue-test-utils 的官方文档中,这个例子有问题,并且对于 vue-test-utils 的描述有待进一步验证。

关于 nextTick 怎么办? Vue 会异步的将未生效的 DOM 更新批量应用,以避免因数据反复突变而导致的无谓的重渲染。这也是为什么在实践过程中我们经常在触发状态改变后用 Vue.nextTick 来等待 Vue 把实际的 DOM 更新做完的原因。 为了简化用法,Vue Test Utils 同步应用了所有的更新,所以你不需要在测试中使用 Vue.nextTick 来等待 DOM 更新。 注意:当你需要为诸如异步回调或 Promise 解析等操作显性改进为事件循环的时候,nextTick 仍然是必要的。