返回 blog
2023年11月22日
3 分钟阅读

vitest 原理

老版本

核心功能

test cli 命令行入口

image.png

读取所有测试文件

执行测试文件(只需 import(filepath))即可

  • 在执行测试文件的过程中,由于开发者使用了vitest暴露的测试套件,如 test, describe 等,vitest就可以通过这些套件,收集到测试用例,然后执行这些用例,最后获取到测试的结果,打印到控制台中

image.png image.png

生命周期hook

在源码中,可以看到各种hook,这些都是测试的生命周期hook,为了开发者更好的扩展测试逻辑 image.png

快照功能

vitest的核心底层库是 chai,其并未实现 snapshot 快照功能,需要vitest自行实现 vitest使用的 jest-snapshot 实现了快照插件,注入到了 chai 中 image.png

断言

是用的chai image.png

spy/mock

vitest 使用 sinon 提供的api实现 image.png

vi.xx

vi对象不过是个vitest的内置工具类实例,它集成了许多实用的三方库,对于开发者而言很方便 image.png vi里面主要是mock、spy、stub等工具方法 image.png

Spy

其中的 spyOn/fn 都是基于 tinySpy 说白了,spy做了函数增强,给函数增加了 调用次数、模拟返回 等等功能 image.png image.png

mock

我之前一直没搞明白,mock是怎么做到的 我今天主要研究了以下两种mock方式

  • 函数
  • 模块

函数 mock

说白了,函数mock就是写了个假函数,其最重要的目的,就是所有使用这个函数的功能,都可以在不写e2e的情况下,模拟流程或功能是否走通 函数mock分为两个

  1. vi.spyOn。监听某个函数,比如监听函数的调用次数、接受参数等
  2. vi.fn。基于spyOn 做了函数增强,可以修改函数的返回值啊之类的

example

vi.spyOn

function getLatest(index = messages.items.length - 1) {
  return messages.items[index]
}

const messages = {
  items: [
    { message: 'Simple test message', from: 'Testman' },
    // ...
  ],
  getLatest, // 也可以是一个 `getter 或 setter 如果支持`
}

it('should get the latest message with a spy', () => {
  // spyOn接受两个参数
  // 监听了getLatest方法
  const spy = vi.spyOn(messages, 'getLatest')
  expect(spy.getMockName()).toEqual('getLatest')

  expect(messages.getLatest()).toEqual(
    messages.items[messages.items.length - 1]
  )

  // 监听到了函数的调用次数
  // tip:其实spyOn对传入的函数做了增强处理,
  // spy有的方法,在原函数上也有
  // 所以 
  expect(messages.getLatest).toHaveBeenCalledTimes(1)
  expect(spy).toHaveBeenCalledTimes(1)

	// 模拟一次函数实现
  spy.mockImplementationOnce(() => 'access-restricted')
  expect(messages.getLatest()).toEqual('access-restricted')

  expect(spy).toHaveBeenCalledTimes(2)
})

这个例子是vitest的官方例子,但是其实可以spyOn非常有用,我们可以监听一些核心功能函数 假设modA里面的getSomeString函数是一个测试的功能点,就可以这样来监听:

export const modA = {
  getSomeString: () => 'some string'
}

import { expect, it, vi } from 'vitest'
import { modA } from './modA'

it('should work', () => {
  // 这是在用例中监听,也可以在 beforeEach中监听
  // 或全局监听
  const spy = vi.spyOn(modA, 'getSomeString')
  
  expect(modA.getSomeString()).toBe('some string')

  expect(spy).toBeCalledTimes(1)
})

vi.fn 跟spyOn相比,fn可以更加灵活,不需要传入函数,自行实现一个假函数

function getLatest(index = messages.items.length - 1) {
  return messages.items[index]
}

const messages = {
  items: [
    { message: 'Simple test message', from: 'Testman' },
    // ...
  ],
  getLatest, // 也可以是一个 `getter 或 setter 如果支持`
}

it('should get with a mock', () => {
  // 传入一个模拟的函数实现
  const mock = vi.fn().mockImplementation(getLatest)

  // 默认情况下,mock的名称是spy
  expect(mock.getMockName()).toEqual('spy')

  expect(mock()).toEqual(messages.items[messages.items.length - 1])
  expect(mock).toHaveBeenCalledTimes(1)

  mock.mockImplementationOnce(() => 'access-restricted')
  expect(mock()).toEqual('access-restricted')

  expect(mock).toHaveBeenCalledTimes(2)

  expect(mock()).toEqual(messages.items[messages.items.length - 1])
  expect(mock).toHaveBeenCalledTimes(3)
})

// 可以在初始化时传入一个函数
// 其实就是把传入的函数做了增强
const getApples = vi.fn(() => 0)

it('should mock', () => {
  getApples()

  expect(getApples).toHaveBeenCalled()
  expect(getApples).toHaveReturnedWith(0)

  getApples.mockReturnValueOnce(5)

  const res = getApples()
  expect(res).toBe(5)
  expect(getApples).toHaveNthReturnedWith(2, 5)
})

vi.fn 同 spyOn,也可以用来全局或局部增强一些核心测试功能函数

模块 mock

我之前一直不懂,为什么可以模拟一个第三方库 今天看了vitest的源码、也去了解了jest的mock模块,才算是明白了其核心原理

vitest通过拦截对第三方库的调用(import ‘some-module’),mock import的所有函数(比如 import axios from ‘axios’,那么axios的所有方法都被mock了) 原来就是这样的,具体是怎么做的?

简单来说,就是利用了vite的插件能力,在transform阶段,魔改三方库代码

export function MocksPlugin(): Plugin {
  return {
    name: 'vitest:mocks',
    enforce: 'post',
    transform(code, id) {
      return hoistMocks(code, id, this.parse)
    },
  }
}

jest 是如何 mock 掉模块的

image.png image.png

image.png image.png jest跟vitest异曲同工!!

import vm from 'node:vm'

// 要执行的 JavaScript 代码字符串
const codeToRun = `
  (function(arg, a) {
    console.log(arg, a)
  })
`

// 在当前上下文中执行代码
const f = vm.runInThisContext(codeToRun)

f('haha', 'aaaa') // haha aaaa

是怎么把axios的所有函数都mock的呢?

image.png image.png

vitest甚至很贴心的导出了mockObject的方法 https://cn.vitest.dev/api/vi.html#vi-importmock image.png image.png

vitest是怎么编译测试文件的代码呢?怎么把测试文件中中请求的第三方包(比如axios),变成mock之后的axios呢? 简单来说,就是vitest先把测试文件中的导入的依赖都编译了,把需要mock的也mock了,然后在下面圈出来的request方法中,请求到这些编译后的依赖。

比如

import axios from 'axios'

会变成

const { default: axios } = await __vite_ssr_dynamic_import__('axios')

其实就是把import给变成了vitest自定义的导入方式

然后在__vite_ssr_dynamic_import__中,调用的是 request方法,然后把模块返回出去(经过了一系列的处理了,比如mock)

image.png image.png

本质上跟jest也是一样的 image.png 把import之类的方法都hack了一份,然后在 runInThisContext 中传下去了image.png image.png