老版本
核心功能
test cli 命令行入口
读取所有测试文件
执行测试文件(只需 import(filepath))即可
- 在执行测试文件的过程中,由于开发者使用了vitest暴露的测试套件,如 test, describe 等,vitest就可以通过这些套件,收集到测试用例,然后执行这些用例,最后获取到测试的结果,打印到控制台中
生命周期hook
在源码中,可以看到各种hook,这些都是测试的生命周期hook,为了开发者更好的扩展测试逻辑
快照功能
vitest的核心底层库是 chai
,其并未实现 snapshot 快照功能,需要vitest自行实现 vitest使用的 jest-snapshot 实现了快照插件,注入到了 chai 中
断言
是用的chai
spy/mock
vitest 使用 sinon 提供的api实现
vi.xx
vi对象不过是个vitest的内置工具类实例,它集成了许多实用的三方库,对于开发者而言很方便 vi里面主要是mock、spy、stub等工具方法
Spy
其中的 spyOn/fn 都是基于 tinySpy 说白了,spy做了函数增强,给函数增加了 调用次数、模拟返回 等等功能
mock
我之前一直没搞明白,mock是怎么做到的 我今天主要研究了以下两种mock方式
- 函数
- 模块
函数 mock
说白了,函数mock就是写了个假函数,其最重要的目的,就是所有使用这个函数的功能,都可以在不写e2e的情况下,模拟流程或功能是否走通 函数mock分为两个
- vi.spyOn。监听某个函数,比如监听函数的调用次数、接受参数等
- 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 掉模块的
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的呢?
vitest甚至很贴心的导出了mockObject的方法 https://cn.vitest.dev/api/vi.html#vi-importmock
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)
本质上跟jest也是一样的 把import之类的方法都hack了一份,然后在
runInThisContext
中传下去了