vue项目增加单元测试

单元测试的作用毋庸置疑能带来很多好处,但是如何去写好单元测试.

单元测试好处

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构

TDD & BDD

TDD(Test Driven Development)测试驱动开发

TDD 的思想是根据需求先写测试用例,依照测试用例再去写功能代码。当增加或者修改某一项需求的时候,需要先修改测试用例,再依照测试用例去修改代码逻辑。

基本步骤

  1. 编写测试用例
  2. 运行测试,测试用例无法通过测试
  3. 编写代码,使测试用例通过测试
  4. 优化代码,完成开发
  5. 重复以上步骤
BDD(Behavior Driven Development)行为驱动开发

与 TDD 相反,BDD 是根据需求先进行开发,等到该功能开发完毕后,再开始编写测试代码进行测试。

基本步骤

  1. 编写代码
  2. 编写测试用例,测试无法通过
  3. 编写代码,使测试用例通过
  4. 优化代码,完成开发
  5. 重复以上步骤

Jest配置

了解到vue-cli构建的项目,在初始化时会询问是否使用单元测试,只需按步骤选择jest即可,会自动安装Vue Test Utils,它是 Vue.js 官方的单元测试实用工具库,为 jestVue 提供了一个桥梁,暴露出一些接口,让我们更加方便的通过 JestVue 应用编写单元测试。

为项目增加单元测试
  1. 新建空白目录,打开终端输入 vue create ‘你的项目名’ 回车创建
    vue create vue-jest
  1. 选择配置
  • Manually select features 是自己手动选择

  • 考虑到以后新项目写,就选择加上了ts, Unit Testing就是单元测试

在这里插入图片描述

  • 其他配置如下图: 主要是单元测试这个选择jest(Pick a unit testing solution: Jest)

在这里插入图片描述

  • 创建成功后执行以下命令就可以通过vscode查看项目目录
 cd vue-jest
 code .
配置jest

在生成的项目根目录下,会有一个 jest.config.json 文件,cli自动生成所用的预设是@vue/cli-plugin-unit-jest。我们可以在这里对 jest 进行个性化的配置。以下是一个配置文档的例子。配置文档具体参数说明
在项目根目录下找到jest.config.json,修改如下,(有些配置已注释,按需打开)

module.exports = {
  preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
  // 开启测试报告
  collectCoverage: true,
  // // 统计哪里的文件
  collectCoverageFrom: ["**/src/components/**","**/src/views/common/**", "!**/node_modules/**"],
  // // 告诉jest针对不同类型的文件如何转义
  transform: {
    "^.+\\.vue$": "vue-jest",
    // '^.+\\.(vue)$': '<rootDir>/node_modules/vue-jest',
    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.jsx?$': 'babel-jest',
    '^.+\\.ts?$': 'ts-jest'
  },
  // 告诉jest需要解析的文件
  moduleFileExtensions: [
      'js',
      'ts',
      'jsx',
      'json',
      'vue'
  ],
  // // 告诉jest去哪里找模块资源,同webpack中的modules
  moduleDirectories: [
      'src',
      'node_modules'
  ],
  // // 告诉jest在编辑的过程中可以忽略哪些文件,默认为node_modules下的所有文件
  transformIgnorePatterns: [
      '<rootDir>/node_modules/'
      + '(?!(vue-awesome|vant|resize-detector|froala-editor|echarts|html2canvas|jspdf))'
  ],
  // // 别名,同webpack中的alias
  // moduleNameMapper: {
  //     '^src(.*)$': '<rootDir>/src/$1',
  //     '^@/(.*)$': '<rootDir>/src/$1',
  //     '^block(.*)$': '<rootDir>/src/components/block/$1',
  //     '^toolkit(.*)$': '<rootDir>/src/components/toolkit/$1'
  // },
  // snapshotSerializers: [
  //     'jest-serializer-vue'
  // ],
  // // 告诉jest去哪里找我们编写的测试文件
  testMatch: [
      '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
      // '**/tests/unit/**/Test.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  // 在执行测试用例之前需要先执行的文件
  // setupFiles: ['jest-canvas-mock']
};

调试单元测试

项目添加了单元测试,里面默认会有一条写好的单元测试,执行npm run test:unit命令,终端就可以看到一条单元测试通过的记录。
在这里插入图片描述

%Stmts(statement coverage): 语句覆盖率,是否每个语句都执行了?
%Branch(branch coverage): 分支覆盖率,是否每个if代码块都执行了?
%Funcs(branch coverage): 函数覆盖率,是否每个函数都调用了?
%Lines(line coverage): 行覆盖率,是否每一行都执行了?

编写单元测试

Vue Test Utils

在编写测试用例之前,我们先来简单了解一下 Vue Test Utils 的 几个 API 以及 Wrapper。

mount: 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
shallowMount: 与mount作用相同,但是不渲染子组件
render: 将一个对象渲染成为一个字符串并返回一个 cheerio 包裹器
createLocalVue:  返回一个 Vue 的类,供你添加组件、混入和安装插件而不会污染全局的 Vue 类
wrapper: 一个 wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
wrapper.vm: 可以访问一个实例所有的方法和属性
wrapper.get() : 返回DOM节点或者Vue组件
wrapper.setData() : 同Vue.set()
wrapper.trigger(): 异步触发事件
wrapper.find(): 返回DOM节点或者Vue组件

具体的 APIWarpper方法和实例 可以参考这里。

一、编写一个TodoApp
1. TodoApp 从一个带有单个 todo的简单组件开始:

在src下新建common文件夹,新建TodoApp.vue文件,编写以下内容

	<template>
	    <div v-for="(todo,key) in todos" :key="key" data-test="todo" 
	    :class="[todo.completed ? 'completed' : '']">
	      {{ todo.text }}
	      <input
	        type="checkbox"
	        v-model="todo.completed"
	        data-test="todo-checkbox"
	      />
	    </div>
	
	    <form data-test="form" @submit.prevent="createTodo">
	      <input data-test="new-todo" v-model="state.newTodo" />
	    </form>
	</template>
	
	<script lang="ts" setup>
		import { reactive } from 'vue';
		
		  let state = reactive({
		    newTodo: ''
		  })
		  
		  let todos = reactive([
		    {
		      text: 'Learn Vue.js 3',
		      completed: false
		    }
		  ])
		
		  const createTodo = () => {
		      todos.push({
		        text: state.newTodo,
		        completed: false
		      })
		  }

	</script>
2. 新建测试文件

在根目录下tests\unit下新建todo.spec.ts文件,首页测试页面默认第一个todo 是否已呈现

	import { mount } from '@vue/test-utils'
	import TodoApp from '@/views/common/TodoApp.vue'
	
	test('renders a todo', () => {
	  // 1. 通过mount创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
	  const wrapper = mount(TodoApp)
	  // 2.通过属性选择器获取该标签
	  const todo = wrapper.get('[data-test="todo"]')
	  // 3.通过text方法获取文本内容,最后断言是否等于Learn Vue.js 3
	  expect(todo.text()).toBe('Learn Vue.js 3')
	})
3. 调试第一个测试用例

执行npm run test:unit
在这里插入图片描述
可以看出我们的测试用例和项目自带的都通过了测试。

4. 添加新的待办事项

一个模块单独写test比较分散,可以利用describe 函数
通过test函数可以创建一个个的测试用例,那当我们的测试用例越来越多的时候,就需要对测试用例进行分类整理,那这就是describe函数的作用。

	describe('todoAll', () => {
		it('renders a todo', () => {
		  const wrapper = mount(TodoApp)
		
		  const todo = wrapper.get('[data-test="todo"]')
		
		  expect(todo.text()).toBe('Learn Vue.js 3')
		})
	
	  it('creates a todo', async () => {
	    const wrapper = mount(TodoApp)
	  	// 1. 通过setValue设置input标签的value是New todo
	    await wrapper.get('[data-test="new-todo"]').setValue('New todo')
	    // 2. 通过trigger触发form表单提交事件
	    await wrapper.get('[data-test="form"]').trigger('submit')
	  	// 3. 判断长度是否等2,等于2代表创建成功了
	    expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
	  })
	})

  1. it和test的效果是一样的,两者语义差不多,只是it语义更好理解
  2. Jest 以同步方式执行测试,一旦调用最终函数就结束测试。然而,Vue 会异步更新 DOM。我们需要标记 test async,并调用await任何可能导致 DOM 更改的方法

执行npm run test:unit,可以看到所有的单测都通过了.
在这里插入图片描述

5. 是否完成代办

可以通过判断div上是否有completed类,代表是否勾选

	describe('todoAll', () => {
		it('renders a todo', () => {
		  const wrapper = mount(TodoApp)
		
		  const todo = wrapper.get('[data-test="todo"]')
		
		  expect(todo.text()).toBe('Learn Vue.js 3')
		})
	
	  it('creates a todo', async () => {
	    const wrapper = mount(TodoApp)
	  	// 1. 通过setValue设置input标签的value是New todo
	    await wrapper.get('[data-test="new-todo"]').setValue('New todo')
	    // 2. 通过trigger触发form表单提交事件
	    await wrapper.get('[data-test="form"]').trigger('submit')
	  	// 3. 判断长度是否等2,等于2代表创建成功了
	    expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
	  })
	  
	   it('completes a todo', async () => {
	    const wrapper = mount(TodoApp)
	  	
	    await wrapper.get('[data-test="todo-checkbox"]').setValue(true)
	  	// 通过classes方法获取标签上所有的类名,然后toContain判断数组里是否包含completed类名
	    expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed')
	  })
	})

执行npm run test:unit,可以看到到复选框选中时,父元素div是有completed,所以通过单测
在这里插入图片描述

6. 动态参数

像上面的单测,里面的内容都是写死的,无法根据实际情况动态修改参数,为了方便测试,把参数抽离出来。调整为外部文件动态传参。以创建todo为例,理想状态是传入一个list参数,和一个期望值,根据list数组的长度去创建n个todo,判断创建todo的长度是否和期望值相等。

在src/views/common下面新建configJS文件夹,然后在新建configData.ts文件.

configData.ts

	let createsTodo = {
	  list: ['a', 'b', 'c'], // 想要动态创建多少个
	  expected: 5  // 期望长度
	}
	
	export default {
	  createsTodo
	}

todo.spec.ts

import { mount } from '@vue/test-utils'
import TodoApp from '@/views/common/TodoApp.vue'
import configData from "@/views/common/configJS/configData"; // 引入测试数据


describe('todoAll', () => {
  it('creates a todo', async () => {
      const wrapper = mount(TodoApp)
      
      configData.createsTodo.list.forEach(async(el: any) => {
        await wrapper.get('[data-test="new-todo"]').setValue(el)
        await wrapper.get('[data-test="form"]').trigger('submit')
      });
      
      expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(configData.createsTodo.expected)
  })
})

执行npm run test:unit
在这里插入图片描述
会发现单测没通过,拿到的不是页面更新后的数据,还是初始数据1,初步判断是async和await失效了,
所以改用nextTick,代码改成如下:

todo.spec.ts

import { mount } from '@vue/test-utils'
import TodoApp from '@/views/common/TodoApp.vue'
import { nextTick } from 'vue'
import configData from "@/views/common/configJS/configData"; // 引入测试数据


describe('todoAll', () => {
  it('creates a todo', async () => {
      const wrapper = mount(TodoApp)
      
      //发现在forEach使用async 和await失效,所以改用nextTick方法
      configData.createsTodo.list.forEach((el: any) => {
        wrapper.get('[data-test="new-todo"]').setValue(el)
        wrapper.get('[data-test="form"]').trigger('submit')
      });
      await nextTick()

      expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(configData.createsTodo.expected)
  })
})

再次执行npm run test:unit
在这里插入图片描述
会发现单测通过了,可list长度是3,expected是4为什么会通过呢,原因是组件原本就有一条数据.

现在我们修改一下期望值,把他改成5,再次执行npm run test:unit

let createsTodo = {
  list: ['a', 'b', 'c'],
  expected: 5
}

发现单测没有通过,但是这是正常的。原因是期望值是错误的.
在这里插入图片描述

7. 多种场景

在数据文件里面写多种场景的数据,怎么才能都实现呢,想到的办法是每个单测场景都传入一个数组,根据数组的长度去循环创建n个单测(test or it).现在以是否完成代办单测为例。
configData.ts

	let completesTodo = [
	  {
	    bool: false, // 是否勾选
	    expected: false // 标签是否有勾选的类名
	  },
	  {
	    bool: true,
	    expected: true
	  },
	]
	
	
	export default {
	  completesTodo,
	}

todo.spec.ts

import { mount } from '@vue/test-utils'
import TodoApp from '@/views/common/TodoApp.vue'
import configData from "@/views/common/configJS/configData";


describe('todoAll', () => {
  configData.completesTodo.forEach(item => {
    it('completes a todo', async() => {
      const wrapper = mount(TodoApp)
    
      await wrapper.get('[data-test="todo-checkbox"]').setValue(item.bool)
      
      if (item.expected) {
        expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed')
      } else {
        expect(wrapper.get('[data-test="todo"]').classes()).not.toContain('completed')
      }
    })
  })

})

执行npm run test:unit
在这里插入图片描述
可以发现根据数组completesTodo的长度循环创建的两条单测都通过了。现在多加一条数据,勾选但是期望值是false。看看是不是通不过单测。
configData.ts

	let completesTodo = [
	  {
	    bool: false, // 是否勾选
	    expected: false // 标签是否有勾选的类名
	  },
	  {
	    bool: true,
	    expected: true
	  },
	  {
		bool: true,
		expected: false
	  },
	]
	
	
	export default {
	  completesTodo,
	}

再次执行npm run test:unit,发现有条单测不通过,符合预期效果.
在这里插入图片描述

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨小凹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值