前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你不知道的 Vue 单元测试(6000字实战单元测试)

你不知道的 Vue 单元测试(6000字实战单元测试)

作者头像
一只图雀
发布2020-10-26 11:16:36
11.4K0
发布2020-10-26 11:16:36
举报
文章被收录于专栏:图雀社区

介绍

Vue-Test-UtilsVue.js 官方的单元测试实用工具库,它提供了一系列的 API 来使得我们可以很便捷的去写 Vue 应用中的单元测试。

主流的单元测试运行器有很多,比如 JestMochaKarma 等,这几个在 Vue-Test-Utils 文档里都有对应的教程,这里我们只介绍 Vue-Test-Utils + Jest 结合的示例。

❝Jest 是一个由 Facebook 开发的测试框架。Vue 对其进行描述:是功能最全的测试运行器。它所需的配置是最少的,默认安装了 JSDOM,内置断言且命令行的用户体验非常好。不过你需要一个能够将单文件组件导入到测试中的预处理器。我们已经创建了 vue-jest 预处理器来处理最常见的单文件组件特性,但仍不是 vue-loader 100% 的功能。 ❞

环境配置

通过脚手架 vue-cli 来新建项目的时候,如果选择了 Unit Testing 单元测试且选择的是 Jest 作为测试运行器,那么在项目创建好后,就会自动配置好单元测试需要的环境,直接能用 Vue-Test-UtilsJestAPI 来写测试用例了。

但是新建项目之初没有选择单元测试功能,需要后面去添加的话,有两种方案:

第一种配置:

直接在项目中添加一个 unit-jest 插件,会自动将需要的依赖安装配置好。

代码语言:javascript
复制
vue add @vue/unit-jest

第二种配置:

这种配置会麻烦一点,下面是具体的操作步骤。

安装依赖

  • 安装 JestVue Test Utils npm install --save-dev jest @vue/test-utils
  • 安装 babel-jestvue-jest7.0.0-bridge.0 版本的 babel-core npm install --save-dev babel-jest vue-jest babel-core@7.0.0-bridge.0
  • 安装 jest-serializer-vue npm install --save-dev jest-serializer-vue

配置 Jest

Jest 的配置可以在 package.json 里配置;也可以新建一个文件 jest.config.js, 放在项目根目录即可。这里我选择的是配置在 jest.config.js 中:

代码语言:javascript
复制
module.exports = {
    moduleFileExtensions: [
        'js',
        'vue'
    ],
    transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'
    ],
    testMatch: ['**/__tests__/**/*.spec.js'],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
}

各配置项说明:

  • moduleFileExtensions 告诉 Jest 需要匹配的文件后缀
  • transform 匹配到 .vue 文件的时候用 vue-jest 处理, 匹配到 .js 文件的时候用 babel-jest 处理
  • moduleNameMapper 处理 webpack 的别名,比如:将 @ 表示 /src 目录
  • snapshotSerializers 将保存的快照测试结果进行序列化,使得其更美观
  • testMatch 匹配哪些文件进行测试
  • transformIgnorePatterns 不进行匹配的目录

配置 package.json

写一个执行测试的命令脚本:

代码语言:javascript
复制
{
    "script": {
        "test": "jest"
    }
}

第一个测试用例

为了保证环境的一致性,我们从创建项目开始一步一步演示操作步骤。

用 vue-cli 创建一个项目

当前我用到的是 3.10.0 版本的 vue-cli。开始创建项目:

代码语言:javascript
复制
vue create first-vue-jest

选择 Manually select features 进行手动选择功能配置:

代码语言:javascript
复制
Vue CLI v3.10.0
┌───────────────────────────┐
│  Update available: 4.0.4  │
└───────────────────────────┘
? Please pick a preset:
  VUE-CLI3 (vue-router, node-sass, babel, eslint)
  default (babel, eslint)
❯ Manually select features

勾选 BabelUnit Testing

代码语言:javascript
复制
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◉ Unit Testing
 ◯ E2E Testing

选择 Jest:

代码语言:javascript
复制
? Pick a unit testing solution:
  Mocha + Chai
❯ Jest

选择 In dedicated config files 将各配置信息配置在对应的 config 文件里:

代码语言:javascript
复制
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
  In package.json

输入n,不保存预设:

代码语言:javascript
复制
? Save this as a preset for future projects? (y/N) n

项目创建完成后,部分文件的配置信息如下:

babel.config.js:

代码语言:javascript
复制
module.exports = {
    presets: [
        '@vue/cli-plugin-babel/preset'
    ]
}

jest.config.js, 这个文件的配置默认是预设插件的,可以按实际需求改成上面提到的配置 Jest 里的配置一样。

代码语言:javascript
复制
module.exports = {
    preset: '@vue/cli-plugin-unit-jest'
}

package.json:

代码语言:javascript
复制
{
    "name": "first-vue-jest",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "test:unit": "vue-cli-service test:unit"
    },
    "dependencies": {
        "core-js": "^3.1.2",
        "vue": "^2.6.10"
    },
    "devDependencies": {
        "@vue/cli-plugin-babel": "^4.0.0",
        "@vue/cli-plugin-unit-jest": "^4.0.0",
        "@vue/cli-service": "^4.0.0",
        "@vue/test-utils": "1.0.0-beta.29",
        "vue-template-compiler": "^2.6.10"
    }
}

执行测试命令

用上面的步骤创建的项目完成项目后,我们可以在 package.jsonscripts 项中看到有个 test:unit ,执行它:

代码语言:javascript
复制
cd first-vue-jest
npm run test:unit

然后终端里会看到输出结果,PASS 表示测试用例通过了,这个是官方提供单元测试例子。下面我们来写点自己的东西。

实现一个 ToDoList

看上面的原型图,有这么几点明确的需求:

  • 在头部右侧输入框输入要做的事情,敲回车后,内容跑到待完成列表里,同时清空输入框
  • 输入框为空的时候敲回车,不做任何变化
  • 待完成列表支持编辑功能,已完成列表不能进行编辑
  • 每个列表项的右侧都有删除按钮,用 - 号表示,点击后删除该项
  • 待完成列表有标记为已完成的按钮,用 号表示,点击后当前项移动到已完成列表
  • 已完成列表有标记为未完成的按钮,用 x 号表示,点击后当前项移动到未完成列表
  • 列表序号从1开始递增
  • 当待完成列表为空的时候,不显示待完成字样
  • 当已完成列表为空的时候,不显示已完成字样

先把上面的页面写好

写页面之前先把创建项目的时候生成的 HelloWorld.vue 和对应的测试文件 example.spec.js 删除;同时修改 App.vue 文件,引入 ToDoList 组件:

代码语言:javascript
复制
<template>
    <div id="app">
        <ToDoList></ToDoList>
    </div>
</template>

<script>
import ToDoList from './components/ToDoList'

export default {
    components: {
        ToDoList
    }
}
</script>

src/compoents 下新建一个文件 ToDoList.vue,样式较多就不贴出来了,具体可以去看本项目源码:

代码语言:javascript
复制
<template>
    <div class="todolist">
        <header>
            <h5>ToDoList</h5>
            <input class="to-do-text" 
                v-model="toDoText" 
                @keyup.enter="enterText" 
                placeholder="输入计划要做的事情"/>
        </header>
        <h4 v-show="toDoList.length > 0">待完成</h4>
        <ul class="wait-to-do">
            <li v-for="(item, index) in toDoList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" @blur="setValue(index, $event)" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToComplete(item, index)">√</span>
                    <span class="del" @click="deleteWait(index)">-</span>
                </p>
            </li>
        </ul>
        <h4 v-show="completedList.length > 0">已完成</h4>
        <ul class="has-completed">
            <li v-for="(item, index) in completedList" :keys="item">
                <p>
                    <i>{{index + 1}}</i>
                    <input :value="item" disabled="true" type="text" />
                </p>
                <p>
                    <span class="move" @click="removeToWait(item, index)">x</span>
                    <span class="del" @click="deleteComplete(index)">-</span>
                </p>
            </li>
        </ul>
    </div>
</template>
代码语言:javascript
复制
<script>
export default {
    data() {
        return {
            toDoText: '',
            toDoList: [],
            completedList: []
        }
    },
    methods: {
        setValue(index, e) {
            this.toDoList.splice(index, 1, e.target.value)
        },
        removeToComplete(item, index) {
            this.completedList.splice(this.completedList.length, 0, item)
            this.toDoList.splice(index, 1)
        },
        removeToWait(item, index) {
            this.toDoList.splice(this.toDoList.length, 0, item)
            this.completedList.splice(index, 1)
        },
        enterText() {
            if (this.toDoText.trim().length > 0) {
                this.toDoList.splice(this.toDoList.length, 0, this.toDoText)
                this.toDoText = ''
            }
        },
        deleteWait(index) {
            this.toDoList.splice(index, 1)
        },
        deleteComplete(index) {
            this.completedList.splice(index, 1)
        }
    }
};
</script>

页面写完,原型上的需求也大概开发完成,页面大概长如下样子:

修改目录配置

接下来就是开始编写单元测试文件了,写之前我们先把测试文件目录修改下为 __tests__,同时修改 jest.config.js 为如下配置,注意其中的 testMatch 已经修改为匹配 __tests__ 目录下的所有 .js 文件了。

代码语言:javascript
复制
module.exports = {
    moduleFileExtensions: [
        'js',
        'vue'
    ],
    transform: {
        '^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleNameMapper: {
        '^@/(.*)$': '<rootDir>/src/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'
    ],
    testMatch: ['**/__tests__/**/*.spec.js'],
    transformIgnorePatterns: ['<rootDir>/node_modules/']
}

编写测试文件

__tests__/unit/ 目录下新建文件 todolist.spec.js,我们约定测试某个 vue 文件,那么它的单元测试文件习惯命名成 *.spec.js*.test.js

代码语言:javascript
复制
import { shallowMount } from '@vue/test-utils'
import ToDoList from '@/components/ToDoList'

describe('test ToDoList', () => {
    it('输入框初始值为空字符串', () => {
        const wrapper = shallowMount(ToDoList)
        expect(wrapper.vm.toDoText).toBe('')
    })
})

上面这个测试文件简要说明:

  • shallowMount 将会创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,只存根当前组件,不包含子组件。
  • describe(name, fn) 这边是定义一个测试套件,test ToDoList 是测试套件的名字,fn 是具体的可执行的函数
  • it(name, fn) 是一个测试用例,输入框初始值为空字符串 是测试用例的名字,fn 是具体的可执行函数;一个测试套件里可以保护多个测试用例。
  • expectJest 内置的断言风格,业界还存在别的断言风格比如 ShouldAssert 等。
  • toBeJest 提供的断言方法, 更多的可以到Jest Expect 查看具体用法。
代码语言:javascript
复制
it('待完成列表初始值应该为空数组', () => {
    const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.toDoList.length).toBe(0)
})

it('已完成列表初始值应该为空数组', () => {
    const wrapper = shallowMount(ToDoList)
    expect(wrapper.vm.completedList).toEqual([])
})

待完成和已完成列表,居然是列表,所以存放数据的字段必须是 Array 类型,空列表就是空数组。如果第二个测试用例改成:

代码语言:javascript
复制
expect(wrapper.vm.completedList).toBe([])

将会报错,因为 toBe 方法内部是调用 Object.is(value1, value2) 来比较2个值是否相等的,和 ===== 的判断逻辑不一样。显然 Object.is([], []) 会返回 false

代码语言:javascript
复制
it('输入框值变化的时候,toDoText应该跟着变化', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.find('.to-do-text').setValue('晚上要陪妈妈逛超市')
    expect(wrapper.vm.toDoText).toBe('晚上要陪妈妈逛超市')
})
it('输入框没有值,敲入回车的时候,无变化', () => {
    const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue('')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length)
})
it('输入框有值,敲入回车的时候,待完成列表将新增一条数据,同时清空输入框', () => {
    const wrapper = shallowMount(ToDoList)
    const length = wrapper.vm.toDoList.length
    const input = wrapper.find('.to-do-text')
    input.setValue('晚上去吃大餐')
    input.trigger('keyup.enter')
    expect(wrapper.vm.toDoList.length).toBe(length + 1)
    expect(wrapper.vm.toDoText).toBe('')
})
  • setValue 可以设置一个文本控件的值并更新 v-model 绑定的数据。
  • .to-do-text 是一个 CSS 选择器;Vue-Test-Utils 提供了 find 方法来通过查找选择器,来返回一个 Wrapper;选择器可以是 CSS 选择器、可以是 Vue 组件也可以是一个对象,这个对象包含了组件的 nameref 属性,比如可以这样用:wrapper.find({ name: 'my-button' })
  • wrapper.vm 是一个 Vue 实例,只有 Vue 组件的包裹器才有 vm 这个属性;通过 wrapper.vm 可以访问所有 Vue 实例的属性和方法。比如:wrapper.vm.data、wrapper.vm.nextTick()。
  • trigger 方法可以用来触发一个 DOM 事件,这里触发的事件都是同步的,所以不必将断言放到 $nextTick() 里去执行;同时支持传入一个对象,当捕获到事件的时候,可以获取到传入对象的属性。可以这样写:wrapper.trigger('click', {name: "bubuzou.com"})
代码语言:javascript
复制
it('待完成列表支持编辑功能,编辑后更新toDoList数组', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['跑步半小时']})
    wrapper.find('.wait-to-do li').find('input').setValue('绕着公园跑3圈') 
    wrapper.find('.wait-to-do li').find('input').trigger('blur') 
    expect(wrapper.vm.toDoList[0]).toBe('绕着公园跑3圈')
})

先用 setDatatoDoList 设置一个初始值,使其渲染出一个列表项;然后找到这个列表项,用 setValue 给其设置值,模拟了编辑;列表项的输入框是用 :value="item" 绑定的 value, 所以 setValue 无法触发更新;只能通过 trigger 来触发更新 toDoList 的值。

代码语言:javascript
复制
it('待完成列表点击删除,同时更新toDoList数组', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['睡前看一小时书']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    wrapper.find('.wait-to-do li').find('.del').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
})
it('点击待完成列表中某项的已完成按钮,数据对应更新', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['中午饭后吃一个苹果']})
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
    wrapper.find('.wait-to-do li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
})
it('点击已完成列表中某项的未完成按钮,数据对应更新', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({completedList: ['唱了一首歌']})
    expect(wrapper.vm.toDoList.length).toBe(0)
    expect(wrapper.vm.completedList.length).toBe(1)
    wrapper.find('.has-completed li').find('.move').trigger('click')
    expect(wrapper.vm.toDoList.length).toBe(1)
    expect(wrapper.vm.completedList.length).toBe(0)
})
it('列表序号从1开始递增', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: ['早上做作业', '下午去逛街']})
    expect(wrapper.vm.toDoList.length).toBe(2)
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>1</i>')
    expect(wrapper.find('.wait-to-do').html()).toMatch('<i>2</i>')
})
it('当待完成列表为空的时候,不显示待完成字样', () => {
    const wrapper = shallowMount(ToDoList)
    wrapper.setData({toDoList: []})
    expect(wrapper.find('h4').isVisible()).toBeFalsy()
    wrapper.setData({toDoList: ['明天去爬北山']})
    expect(wrapper.find('h4').isVisible()).toBeTruthy()
})

一个测试用例中可以写多个 expect 以保证断言的准确性。

异步测试

最后我们为了模拟异步测试,所以加一个需求,即页面加载的时候会去请求远程待完成列表的数据。在项目根目录新建 __mocks__ 目录,同时新建 axios.js:

代码语言:javascript
复制
const toToList = {
    success: true,
    data: ['上午去图书馆看书', '下去出去逛街']
}

export const get = (url) => {
    if (url === 'toToList.json') {
        return new Promise((resolve, reject) => {
            if (toToList.success) {
                resolve(toToList)
            } else {
                reject(new Error())
            }
        })
    }
}

修改 ToDoList.vue,导入 axios 和增加 mounted

代码语言:javascript
复制
<script>
import * as axios from '../../__mocks__/axios'
export default {
    mounted () {
        axios.get('toToList.json').then(res => {
            this.toDoList = res.data
        }).catch(err => {
            
        })
    },
};
</script>

测试用例编写为:

代码语言:javascript
复制
it('当页面挂载的时候去请求数据,请求成功后应该会返回2条数据', (done) => {
    wrapper.vm.$nextTick(() => {
        expect(wrapper.vm.toDoList.length).toBe(2)
        done()
    })
})

对于异步的代码,写断言的时候需要放在 wrapper.vm.$nextTick() 里,且手动调用 done()

配置测试覆盖率

测试用例写了部分,如果我们看下覆盖率如何,就需要要配置测试覆盖率。在 jest.config.js 里新增配置:

代码语言:javascript
复制
collectCoverage: true,
collectCoverageFrom: ["**/*.{js,vue}", "!**/node_modules/**"],

package.jsonscripts 中新增一条配置:

代码语言:javascript
复制
"test:cov": "vue-cli-service test:unit --coverage"

然后我们在终端运行: npm run test:cov,结果如下:

运行测试覆盖率命名后会在项目根目录生成 coverage 目录,浏览器打开里面的 index.html

代码语言:javascript
复制
● JavaScript 测试系列实战(一):使用 Jest 和 Enzyme 测试 React 组件● 你不知道的 Npm(Node.js 进阶必备好文)● 用动画和实战打开 React Hooks(一):useState 和 useEffect
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 图雀社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 环境配置
    • 安装依赖
      • 配置 Jest
        • 配置 package.json
        • 第一个测试用例
          • 用 vue-cli 创建一个项目
            • 执行测试命令
            • 实现一个 ToDoList
              • 先把上面的页面写好
                • 修改目录配置
                  • 编写测试文件
                    • 异步测试
                      • 配置测试覆盖率
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档