一.Jest 单元测试术语解析:describe、it、expect、test

jest测试代码片段

describe("number test", ()=>{
	it('1 is true', ()=>{
		expect(1).toBeTruthy()
	})
	test('2 is true',()=>{
		expect(2).toBeTruthy()
	})
})

  • describe 描述, decribe会形成一个作用域
  • it 断言
  • expect 期望
  • test 测试,类似it

二. Vue Test Utils

Vue Test Utils 是 Vue.js 官方的单元测试实用工具库。它提供了一系列非常方便的工具,使我们更加轻松的为Vue构建的应用来编写单元测试。主流的 JavaScript 测试运行器有很多,但 Vue Test Utils 都能够支持。它是测试运行器无关的。

简单的理解:Vue-test-utils在Vue和Jest之前提供了一个桥梁,暴露出一些接口,让我们更加方便的通过Jest为Vue应用编写单元测试

三. 常用的API

mount挂载组件

创建一个包含被挂载和渲染的 Vue 组件的 Wrapper

import { mount } from "@vue/test-utils";
import Counter from "@/views/Counter.vue";
// 通过npm install sinon安装
import sinon from "sinon";

describe("Counter.vue", () => {
    const change = sinon.spy();
    // 监听 Counter 里面的改变事件
    const wrapper = mount(Counter, {
        listeners: {
            change
        }
    });

 // 现在挂载组件,你便得到了这个包裹器
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {

 //验证该组件渲染出来的 HTML 符合预期'<span class="count">0</span>'
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 也便于检查已存在的元素
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })

});

shallowMount

和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,不同的是被存根的子组件

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('返回一个 div', () => {
    const wrapper = shallowMount(Foo)
    expect(wrapper.contains('div')).toBe(true)
  })
})

createLocalVue

createLocalVue 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
可通过 options.localVue 来使用

import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const localVue = createLocalVue()
const wrapper = shallowMount(Foo, {
  localVue,
  mocks: { foo: true }
})
expect(wrapper.vm.foo).toBe(true)

const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)

伪造 $route 和 $router

有的时候你想要测试一个组件在配合 $route 和 $router 对象的参数时的行为。这时候你可以传递自定义假数据给 Vue 实例。

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/home'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

expect(wrapper.vm.$route.path).toBe('/home')

stubs

stubs 选项覆写全局或局部注册的组件

子组件触发自定义事件 - wrapper.find(子组件).vm.$emit(‘自定义事件’)`

触发 dom 事件 -wrapper.trigger(‘事件’)

四 DOM 结构

Wrapper.find(选择器)

  • 返回匹配选择器的第一个 Wrapper(DOM 节点或 Vue 组件)

Wrapper.findAll(选择器)

  • 返回一个 WrapperArray

Wrapper.findAll(选择器).at(序号)

  • 返回 WrapperArray 中的第 indexWrapper(从 0 开始计数)

Wrapper.is(选择器)
判断 Wrapper 是否匹配选择器

Wrapper.contains(选择器)

  • 判断 Wrapper 是否包含了一个匹配的选择器

Wrapper.exists()

  • 判断 WrapperWrapperArray 是否存在

Wrapper.html()

  • 返回 Wrapper DOM 节点的 HTML 字符串

五.选择器

很多方法的参数中都包含选择器。一个选择器可以是一个 CSS 选择器、一个 Vue 组件或是一个查找选项对象。

Vue 组件

标签选择器 (div、foo、bar)

CSS 选择器

类选择器 (.foo、.bar)
特性选择器 ([foo]、[foo=“bar”])
id 选择器 (#foo、#bar)
伪选择器 (div:first-of-type)
近邻兄弟选择器 (div + .foo)
一般兄弟选择器 (div ~ .foo)

const buttonr = wrapper.find('.button')
const content = wrapper.find('#content')

查找选项对象

Name:可以根据一个组件的name选择元素。wrapper.find({ name: ‘my-button’ })
Ref:可以根据$ref选择元素。wrapper.find({ ref: ‘myButton’ })

而findAll返回的是一个数组,在选择有多个元素的情况下是不可以使用find的,在使用findAll后需要使用at()来选择具体序列的元素。

在得到了我们的DOM元素之后我们就可以很方便地对属性以及内容进行断言判断。
这里提一句,有关于样式的测试我更偏向于在E2E测试中去断言而不是在单元测试,这显得会更为直观,当然在单元测试中也提供了抓取class的API。
有关于DOM的API列出了以下几个

  • attributes: 属性
  • classes:wrapper.classes()返回一个字符串数组,wrapper.classes(‘bar’)返回一个布尔值
  • contains:返回包含元素或组件匹配选择器
  • html: 以字符串形式返回DOM节点的HTML

六 断言匹配器(常用)

.toBe 检查值相等
.toEqual 检查对象的值
.toMatch 检查字符串
.toContain 检查数组
.toBeCloseTo 检查浮点数
toBeCalled检查方法被调用(toHaveBeenCalled的别名).toBeCalledWith检查方法被调用时的参数.not` 取反

七. 常用技巧

下面简单列举几个,其他的请查看官网

1.明白要测试的是什么
不管业务内容多繁琐,只关注其输入和输出。

2.测试组件渲染出来的 HTML

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Counter', () => {
 // 现在挂载组件,你便得到了这个包裹器
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {

 //验证该组件渲染出来的 HTML 符合预期'<span class="count">0</span>'
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 也便于检查已存在的元素
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

3.模拟用户交互
wrapper.find() 定位该按钮,此方法返回一个该按钮元素的包裹器。然后我们能够通过对该按钮包裹器调用 .trigger() 来模拟点击。

it('button click should increment the count', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

4.使用 nextTick 编写异步测试代码
更新会引发 DOM 变化的属性后必须等待一下。你可以使用 Vue.nextTick()

it('updates text', async () => {
  const wrapper = mount(Component)
  await wrapper.trigger('click')
  expect(wrapper.text()).toContain('updated')
  await wrapper.trigger('click')
  wrapper.text().toContain('some different text')
})

// 或者你不希望使用 async/await
it('render text', done => {
  const wrapper = mount(TestComponent)
  wrapper.trigger('click').then(() => {
    wrapper.text().toContain('updated')
    wrapper.trigger('click').then(() => {
      wrapper.text().toContain('some different text')
      done()
    })
  })
})

5.仿造 Prop
使用 Vue 在内置 propsData 选项向组件传入 prop:

import { mount } from '@vue/test-utils'

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

你也可以用 wrapper.setProps({}) 方法更新这些已经挂载的组件的 prop。想查阅所有选项的完整列表,请移步该文档的挂载选项章节。

6.测试键盘、鼠标等其它 DOM 事件
触发事件:Wrapper 暴露了一个 trigger 方法。它可以用来触发 DOM 事件。 wrapper.find(‘button’)

八 常见的场景

九 . vue结合Jest进行单元测试

  • axios 测试(api 测试)
  • vue-router 测试
  • vuex 测试
  • eventhub 测试
  • emit 测试
  • computed 测试
  • filter 测试
  • watch 测试
  • 生命周期测试
  • window 全局方法 (以 open/localStorage/定时器 为例)
  • 自定义组件测试(测试props, 插槽功能等)

Vue引入Jest

(1)使用vue-cli脚手架进行jest配置

> vue create vue-jest
> cd vuejest
> vue add @vue/cli-plugin-unit-jest
> npm install

(2) 安装完成后package.json里会增加这些依赖
在这里插入图片描述
可以在后面加上:“test:unit”: “vue-cli-service test:unit --watchAll” 启动监听模式

  • 项目目录会增加出一个jest.config.js配置文件,根据项目需要修改配置
    参考API官网
module.exports = {
	//用作Jest配置基础的预设
	preset: '@vue/cli-plugin-unit-jest',
	//模块使用的文件扩展名数组。默认: ["js", "json", "jsx", "ts", "tsx", "node"] 如果您需要模块而未指定文件扩展名,则这些是Jest将按从左到右的顺序查找的扩展名。
	moduleFileExtensions: ["js", "jsx", "json", "vue"],
	moduleNameMapper: {
		"^@/(.*)$": "<rootDir>/src/$1"
	}
}

补充:

首先我们需要安装jest需要的一些插件:

  • jest: Jest
  • @vue/test-utils:Vue Test Utils 是Vue.js 官方的单元测试实用工具库
  • babel-jest:使用Babel自动编译JavaScript代码
  • vue-jest:使用vue-jest去编译.vue 文件
  • jest-serializer-vue:生成vue快照的序列化器的模块,进行snapshot tests会需要
  • jest-transform-stub:处理css|图片|字体的预处理器jest-sonar-reporter(可选):
  • Jest的自定义结果处理器.处理器将Jest的输出转换为Sonar的通用测试数据格式.
module.exports = {
  verbose: true,
  bail: 1,
  moduleFileExtensions: [
    'vue',
    'js',
    'json'
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '.+\\.(css|less|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.js$': 'babel-jest',
    '.*\\.vue$': 'vue-jest',
    '^.+\\.svg$': 'jest-svg-sprite-loader'
  },
  snapshotSerializers: [ 'jest-serializer-vue' ],
  testResultsProcessor: 'jest-sonar-reporter',
  collectCoverage: true,

  collectCoverageFrom: [
    'src/**/*.{js,vue}',
  ],
  coverageReporters: ['html', 'lcov', 'text-summary'],

  coverageDirectory: './test/coverage',
  coveragePathIgnorePatterns: [ '/node_modules/' ],
  coverageThreshold: {
    global: {
      branches: 20,
      functions: 20,
      lines: 20,
      statements: 20
    }
  },
  testMatch: [
    '**/*.spec.js'
  ],
};


  • verbose: 多于一个测试文件运行时展示每个测试用例测试通过情况
  • bail: 参数指定只要有一个测试用例没有通过,就停止执行后面的测试用例
  • moduleFileExtensions: jest 需要检测测的文件类型
  • moduleNameMapper: 从正则表达式到模块名称的映射,和webpack的alisa类似
  • transform: 预处理器配置
  • snapshotSerializers: Jest在快照测试中使用的快照序列化程序模块的路径列表
  • testResultsProcessor: 自定义结果处理器,用jest-sonar-reporter输出sonar需要的通用测试数据格式
  • collectCoverage: 是否进行覆盖率收集
  • collectCoverageFrom: 需要进行收集覆盖率的文件,会依次进行执行符合的文件
  • coverageReporters: Jest在编写覆盖率报告的配置,添加"text"或"text-summary"在控制台输出中查看覆盖率摘要
  • coverageDirectory: Jest输出覆盖信息文件的目录
  • coveragePathIgnorePatterns: 需要跳过覆盖率信息收集的文件目录
  • coverageThreshold: 覆盖结果的最低阈值设置,如果未达到阈值,jest将返回失败
  • testMatch: Jest用于检测测试的文件,可以用正则去匹配

(3)项目目录多时会多出一个单元测试tests/uiit文件夹及样例测试代码

在这里插入图片描述
这边已经有一个针对helloword的测试用例
在这里插入图片描述
使用@vue/test-utils
shallowMount 单元测试 :会使用占位符占住HelloWorld的子组件,只渲染HelloWorld这个组件

十.简单demo

1.测试监听器及模拟点击事件

<template>
	<div>
		<span>count: {{ count }}</span>
		<button @click="handleClick">count++</button>
	</div>
</template>

<script>
export default {
	data() {
		return {
			count: 0
		};
	},
	methods: {
		handleClick() {
			this.count++;
			this.$emit("change", this.count);
		}
	}
};
</script>

<style></style>


编写单元测试代码Counter.spec.js

import { mount } from "@vue/test-utils";
import Counter from "@/views/Counter.vue";
import sinon from "sinon";

describe("Counter.vue", () => {
    const change = sinon.spy();
    const wrapper = mount(Counter, {
        listeners: {
            change
        }
    });
    it("renders counter html", () => {
        expect(wrapper.html()).toMatchSnapshot();
    });
    it("count++", () => {

        const button = wrapper.find("button");
        button.trigger("click");
        console.log(wrapper.vm.count, change.called, change.callCount, 'aaaaa')
        expect(wrapper.vm.count).toBe(1);
        expect(change.called).toBe(true);
        button.trigger("click");
        console.log(wrapper.vm.count, change.called, change.callCount, 'bbbb')
        expect(change.callCount).toBe(2);
        // 错误测试
        // button.trigger("click");
        // console.log(wrapper.vm.count, change.called, change.callCount, 'ccc')
        // expect(change.callCount).toBe(2);
    });
});


注意如果的click事件是子组件(按钮组件 e m i t ) 的 事 件 , 在 父 组 件 内 不 属 于 D O M 原 生 事 件 , 所 以 触 发 方 式 不 能 使 用 t r i g g e r , 而 应 该 使 用 emit)的事件,在父组件内不属于DOM原生事件,所以触发方式不能使用trigger,而应该使用 emit),DOM,使trigger,使emit

2. 测试DOM结构
通过mount、shallow、find、findAll方法都可以返回一个包裹器对象,包裹器会暴露很多封装、遍历和查询其内部的Vue组件实例的便捷的方法。

其中,find和findAll方法都可以都接受一个选择器作为参数,find方法返回匹配选择器的DOM节点或Vue组件的Wrapper,findAll方法返回所有匹配选择器的DOM节点或Vue组件的Wrappers的WrapperArray。
在这里插入图片描述
注意:红色框,获取到正确的wrapper.classes() 的值

3.测试Props

父组件向子组件传递数据使用Props,而子组件向父组件传递数据则需要在子组件出发父组件的自定义事件

当测试对父组件向子组件传递数据这一行为时,我们想要测试的当我们传递给子组件一个特定的参数,子组件是否会按照我们所断言的那样变现。

在初始化时向子组件传值,使用的方法是propsData。
在这里插入图片描述

也可以使用setProps方法:

 it('renders props.msg when passed', async () => {
        const msg = 'new message';
        const wrapper = mount(Setprops)
        await wrapper.setProps({ msg: 'new message' })

        console.log("wrapper.text():", wrapper.text())
        expect(wrapper.text()).toMatch(msg)
    })

4.测试方法

单元测试的核心之一就是测试方法的行为是否符合预期,在测试时要避免一切的依赖,将所有的依赖都mock掉。

这个例子里面,我们仅仅关注测试getAnswer方法,其他的忽略掉。

十一. 补充

(1)测试辅助工具Sinon

Sinon是用来辅助我们进行前端测试的,在我们的代码需要与其他系统或者函数对接时,它可以模拟这些场景,从而使我们测试的时候不再依赖这些场景。
Sinon有主要有三个方法辅助我们进行测试:spy,stub,mock。

安装sinon
npm install --save-dev sinon

  1. sinon.spy()
    spy一般有两种玩法,一种是生成一个新的匿名间谍函数,另外一种是对原有的函数进行封装并进行监听。
 const wrapper = mount(Counter, {
        listeners: {
            change
        }
    });

sinon.spy()会产生一个函数对象,当调用这个函数对象后,这个函数对象通过called可以返回一个bool值,表示函数是否被调用。
expect(change.called).toBe(true); // console.log(change.called) 是true ,所以断言匹配就是true 正确

  1. stub
    是带有预编程行为的函数,就是spy的加强版,不仅完全支持spy的各种操作,还能操作函数的行为。和spy一样,stub也能匿名,也能去封住并监听已有函数。然而有一点和spy不同,当封装了一个已有函数后,原函数不会再被调用。
    使用stub来嵌入或者直接替换掉一些代码,来达到隔离的目的。简单的说,stub是代码的一部分。在运行时用stub替换真正代码,忽略调用代码的原有实现。目的是用一个简单一点的行为替换一个复杂的行为,从而独立地测试代码的某一部分。
    在这里插入图片描述
    sinon.stub(HTMLMediaElement.prototype, “play”):将HTMLMediaElement.prototype.play 替换成一个stub(),可将它替换成指定函数
    stub.restore():由于 stub 是使用指定函数替换已有的函数,所以每次使用后需要用stub.restore()复原它

  2. mock
    像spy和stub一样的伪装方法,如果mock没有得到期望的结果就会测试失败

十二 参考

Logo

前往低代码交流专区

更多推荐