相关文章推荐

vue3 + vite 单元测试编写经验分享(二)

10 个月前 · 来自专栏 财神驾到

上一篇文章谈了些想法,这篇文章讲介绍工具。

想写测试用例,先来看看测试工具为我们提供了哪些方法。

我们会用到两个工具:

一、vitest

Vitest 是一个由 Vite 提供支持的极速单元测试框架。

官网地址: Vitest

他是个框架,什么是框架,就是提供一个基础测试环境的。

这个环境包含了哪些功能呢?

测试常用的功能:

1、测试分组

一个函数的测试用例,可能需要写很多个,需要做个分组,比如:

import { describe, expect, test } from 'vitest'
const person = {
  isActive: true,
  age: 32,
describe('person', () => {
  test('person is defined', () => {
    expect(person).toBeDefined()
  test('is active', () => {
    expect(person.isActive).toBeTruthy()
  test('age limit', () => {
    expect(person.age).toBeLessThanOrEqual(32)


2、断言

用来判断结果与预期是否一致 expect(**).toBe(**)

import { expect } from 'vitest'
const input = Math.sqrt(4)
expect(input).toBe(2) // jest AP

除了 toBe 判断值是否相等,还有很多其他的判断方式。

toEqual 断言检查值是否等于接收值,或者是同样的结构,如果是对象类型(将会使用递归的方法进行比较

toStrictEqual 断言检查值是否等于接收值或者同样的结构,如果是对象类型(将会使用递归的方法进行比较),并且会比较它们是否是相同的类型

toContain 用于断言检查值是否在数组中。还可以检查一个字符串是否为另一个字符串的子串。

toHaveLength 用于断言一个对象是否具有 .length 属性,并且它被设置为某个数值。

toHaveProperty 用于断言对象上是否存在指定 key 的属性。


具体的 api 用法请查看官方文档 cn.vitest.dev/api/#




3、代理

代理很有用,使用代理你可以屏蔽掉你不想测试的模块,可以营造出一个可靠的环境。

比如页面中有个请求数据的接口,这个接口在单元测试过程中,是不应该触发的。

那就可以使用代理,让这个接口直接返回一个值,不走真实的接口请求。


通过代理返回不同的结果,还可以模拟很多场景,空值的场景,error 的场景。


import { getListData } from '@/api/a'

代理有两个方法

  • mock
vi.mock('@/api/a', () => {
  return {
    getListData: () => Promise.then({data: {a: 1, b: 2}})

这样页面中请求 getListData, 得到的就是 {data: {a: 1, b: 2}} 结果

详情可自行查看文档。


mock 有个致命的缺点,他是一开始最先被初始化的,所以外部声明的变量,他内部是获取不到的。什么意思呢?就是下面的代码,测试用例执行会报错:

a.ts

import { a } from '@/api'
export function fn() {
    return a()

a.spec.ts

import { vi, expect, describe, test } from 'vitest'
import { fn } from '../a'
describe('test', () => {
    const mockData  = []
    test('test1', () => {
        vi.mock('@/api', () => {
            return {
                a: () => mockData 
        expect(fn()).toStrictEqual(mockData)

mockData is not defined

mock 会被提取到最外层,最开始初始化时便执行,这时候还没有执行 const mockData =[] 呢

这里可以将 const mockData =[] 放入 vi.mock 逻辑内部,但外部的 expect 就引用不到 mockData 了。


那怎么办?官方没打算解决。

我觉得有两个办法解决

1、分别声明两个变量,一个给 mock 用,一个给 expect 用。

2、在 mock 中声明变量,绑定到 window(或 global)上,expect 使用时,从 window 上取即可。


  • spyOn
在对象的方法或 getter/setter 上创建一个监听

看一下官方的例子:

let apples = 0
const obj = {
  getApples: () => 13,
const spy = vi.spyOn(obj, 'getApples').mockImplementation(() => apples)
apples = 1
expect(obj.getApples()).toBe(1)
expect(spy).toHaveBeenCalled()
expect(spy).toHaveReturnedWith(1)

看起来 spyOn 也可以用来做代理,上面的 mockData 案例,用它写一下:

import * as API from '@/api'
const mockData = []
vi.spyOn(API, 'a').mockReturnValue(mockData)
expect(fn()).toStrictEqual(mockData)


看上面的例子,就发现有些时候用 spyOn 比 mock 要方便些


spyOn 也有缺点,他只能代理原对象上已有的属性,此属性不能是 readonly 的,此属性需要是个函数,不能单纯是个值。


spyOn 返回的是 MockInstance, 他有很多个方法,比如上面用的 mockReturnValue 方法,指模拟函数调用时将返回的值。

常用的还有:

mockResolvedValue 当异步函数被调用时,接收一个将被决议 ( resolve ) 的值。

mockRejectedValue 当异步函数被调用时,接收一个将被拒绝 ( reject ) 的错误。

上面两个方法,是代理接口时常用的方法。


4、监听

有时我们需要监听一个函数是否被调用。(很大概率与 代理 搭配着使用)

比如一个页面初始化时,会调用一个接口,我要测试这个功能:初始化时是否调用了接口?

let apples = 0
const obj = {
  getApples: () => 13,
const spy = vi.spyOn(obj, 'getApples').mockImplementation(() => apples)
apples = 1