当前位置: 首页 > 科技观察

Vitest:替代 Jest 的前端测试工具新选择

时间:2023-03-15 23:51:47 科技观察

Vitest:替代JestGroup的前端测试工具新选择)。最终,单从性能上来说,还是取得了不错的成绩,也大大减轻了jest臃肿带来的很多配置的脑力负担。同时我也发现社区里关于vitest的文章还是比较少的。因此,在这篇文章中,笔者将为大家介绍vitest的测试框架,以及从jest迁移到vitest过程中的一些坑记录。我希望能有所帮助。vitest定位是一个高性能的前端单元测试框架,具体官网地址可以参考:https://vitest.dev/。目前社区也有一些明星开源项目。比如vite就是用vitest作为测试框架来“吃狗粮”(具体可以参考pr:https://github.com/vitejs/vite/pull/8076)Vitest除了对性能有比较大的提升外相比jest,它还提供了很好的ESM支持。不过目前vitest官方并没有给出具体的benchmark进行对比,但是在其官方推特频道上可以看到很多使用migration的用户在速度上有了很大的提升:特性介绍可以先在vitest官网关于其关键特性的一些介绍,这里我就带大家了解一下我认为比较重要的一些比较重要和实用的特性。ESM优先支持ESM是目前前端模块的一个未来发展趋势,越来越多的包正在以esm格式打包输出产品,比如社区中比较知名的ora、chalk等库。ESM和CJS的打包产品格式可以参考antfu的这篇文章:https://antfu.me/posts/publish-esm-and-cjs。但是,很多项目还在使用CJS,很多项目开始迁移到ESM。不过jest这个目前主流的测试框架,总之是支持ESM的。包括上面提到的vite仓库本身从jest迁移到vitest也是jest本身的esm支持问题导致的:关于jest原生支持esm,可以参考这个issue:https://github.com/facebook/jest/issues/9430和vitest自然对ESM有更好的支持。它的底层将使用esbuild来转换文件。但是由于ESM的优先支持,也给vitest带来了很多“问题”。后续介绍迁移时会详细说明。Vite同步的配置文件对于原本使用vite作为构建工具的项目可能是有好处的,因为本质上一个配置文件是可以复用的。比如一个项目使用了vite.config.ts,那么可以直接配置vitest相关配置就可以了,比如:import{defineConfig}from'vitest/config';exportdefaultdefineConfig({test:{//...}});但是,对于不是用vite构建的项目,需要直接新建一个配置文件,但是需要注意的是,使用最新版本的vitest不需要用户在项目中安装vite。如果你只是使用vitest,那么你只需要安装vitest。当然,如果你想使用一个单独的测试配置而不是与vite共享一个相应的构建配置,你可以使用一个名为vitest.config.ts的配置文件,vitest会使用这个文件作为最高优先级的配置。内置的TypeScript/JSX支持一般的jest用户。如果他们需要测试ts或者tsx的代码逻辑,一般需要使用ts-jest,需要在项目中添加一个配置,比如一个jest.config.js的配置:module.exports={transform:{'^.+\.(t|j)sx?$':'ts-jest',},globals:{'ts-jest':{tsconfig:`${__dirname}/tsconfig.test.json`,},},};其实现在很多应用都是用TS来开发的。使用jest每次都会增加一些多余的配置和导入额外的包,但是如果使用vitest,就没有这些方面的负担了。与实时观看模式相比,这是vitest比较大的优势。watch模式下test的热更新比jest快多了。至于为什么vitest的watch模式这么快,可以参考antfu的一个twitter内容(https://twitter.com/antfu7/status/1468233216939245579):图片和vite的原理差不多,vitest知道每个模块应用程序所依赖的,因此它可以清楚地决定在文件更改后重新运行哪些模块的测试内容。这对于开发中的模块测试非常实用。vitest的使用&迁移前面介绍了vitest的一些亮点。下面介绍一下vitest的使用和操作。这里我们先不做简单的demo。这些内容在官方文档中比较容易找到。我不会在这里讨论它们。做太多的扩张。事实上,vitest的整体API与Jest是比较一致的。如果要迁移一些比较小的项目,vitest官方提供了相关的迁移过程文档:https://vitest.dev/guide/migration。html#migrating-from-jest。在此,笔者结合自己在迁移过程中踩过的一些坑,对vitest的使用和迁移进行更准确的介绍,希望对有这方面需求的读者有所帮助。适配全局APIJest默认开启全局API访问,vitest默认关闭。因此,如果不开启,访问测试文件中一些vitest相关的API会报错。默认情况下,必须这样写://需要导入APIimport{describe,expect}from'vitest';describe('test',()=>{expect(1+1).toBe(1);})如果你的项目之前使用过Jest,迁移过程中需要重新导入很多文件。简单的解决办法是在相应的config文件中开启globalsAPI的访问,tsconfig也需要设置相应的访问类型//vitest.config.tsimport{defineConfig}from'vitest/config';exportdefaultdefineConfig({test:{globals:true}})//tsconfig.json{"compilerOptions":{"types":["vitest/globals"]}}这允许你使用类似于Jest的全局测试API.Jest相关的API和类型替换基本上很多Jest相关的API都可以直接替换,例如:jest.mock()jest.fn()jest.spyOn()//这种类型的API可以直接替换对于vi。mock()vi.fn()vi.spyOn()如果图比较简单,我们可以在vitest的setUp脚本中直接替换globaljest,这里不推荐,如果只是短期替换,还可以.//vitest.config.tsimport{defineConfig}from'vitest/config';exportdefaultdefineConfig({test:{setupFiles:['./vitest.setup.ts']}});//vitest.setup.tsif(!global.jest){global.jest=vi;}当然Jest中有一些特殊的API在vitest中是不支持的,这里后面会介绍,然后相关的Type声明调整,还是有一些区别的vitest和Jest的一些通用类型,比如返回值类型反转{SpyInstance,Mock}from'vitest';letvitestFn:MockletvitestFn:SpyInstance具体可以参考vitest的迁移文档就可以了。别名相关的配置替换一般如果使用tsconfig中的paths配置,还需要在jest中通过配置声明别名配置,否则jest在测试时会识别不到项目中写的路径,例如,通用配置://jest.config.jsmodule.exports={roots:['/src'],moduleNameMapper:{'^src/(.*)':'/src/$1'}}Generalforthiscategoryvitest中的名称处理需要借助vite相关配置来完成://vitest.config.tsimport{defineConfig}from'vitest/config';importpathfrom'path';exportdefaultdefineConfig({resolve:{alias:{src:path.resolve(__dirname,'src')}}})基本等同于上面的jestalias处理,而且由于vitest底层是基于vite的(源码使用了vite的createServer方法),所以vite中的很多配置都可以等价的导入到vitest中。snapshotSerializersCompatibility在jest中提供了一些snapshots的序列化配置,例如://jest.config.jsmodule.exports={snapshotSerializers:['jest-serializer-path']}是vitest中此类库的接口和exportof数据类型是兼容的,所以我们其实可以在vitest中直接使用jest对应的快照序列化库。具体使用方法请参考文档:https://vitest.dev/guide/migration。html#migrating-from-jest使用前面提到的setup文件的相关配置://vitest.setup.tsimportserializerfrom'jest-serializer-path';expect.addSnapshotSerializer(序列化器);迁移踩坑和workaround上面这部分主要介绍了将项目从jest整体迁移到vitest需要做些什么,但实际上做了这些事情之后,你的项目还是无法运行测试。在这里,笔者就说一下在实际迁移过程中遇到的问题。您好,希望对您有所帮助。库产品的CJS引用抛出错误。前面提到,vitest是一个基于ESMFirst的测试框架。其实在某种程度上,它并不支持CJS和ESM的一些混合使用。这里的问题是,monorepo下分包生产出来的产品内容是cjs,因为vitest底层是基于vite的,vite本身会使用esbuild改造一些库文件,这里cjs的代码会是处理为esm,然后这里会出现Athrowerror。笔者这里的处理方法比较简单,直接将CJS导出的包改成ESM格式,因为是Monorepo内部使用的包,所以在这里修改并没有特别大的风险。不过笔者也在社区看到一些从业者对cjs和esm的混合使用提供了一些workaround。具体可以参考这篇文章:https://blog.csdn.net/qq_21567385/article/details/124742193。不过这里更推荐的方式是先拥抱原生的esm,再尝试vitest,这样会更好。cross-workspacereferencestoconstenumthrowerrors如果你当前的测试包引用了其他包中的constenum类型变量,在vitest下进行transform时会变成undefined。例如:import{TestConst}from'@test/shared'console.log(TestConst.TestA)//@test/sharedpackageexportconstenumTestConst{TestA='test_a',}在vitest中测试时会抛出错误:TypeError:无法读取未定义的属性“TestA”。这里前面提到,因为vitest底层的transform工具是esbuild,esbuild目前不支持从第三包导入的constenum语句的导入编译,参考issue:https://github.com/evanw/esbuild/问题/128。在vitest的discord中和vitest的核心开发者沟通后,发现这个问题确实是vite本身的一些限制导致的:所以这里的解决方法其实很简单,修改第三个包的constenum为enum即可,这实际上并不会造成特别大的体积损失。笔者这里是内部的Monorepo包,所以调整起来也很简单。vi.mock导致moduleundefined如果在使用了vi.mock()的测试文件中导入其他方法,在mock中使用,很大程度上是无法在mock上获取到这些方法的,例如:import{mocktest}from'../test-a';describe('Test',()=>{it('xxx',async()=>{vi.mock('@test-shared',()=>{getTestFunc:vi.fn(),mocktest})})})很大程度上会因为获取不到mocktest而抛出ReferenceError。具体可以参考vitest的相关issue:https://github.com/vitest-dev/vitest/issues/1336。vitest的coreworkers给出的意见是在这种情况下用vi.doMock()代替vi.mock(),因为vi.mock()会被提升到顶层而忽略其他import:同样有一些其他奇怪的mock问题也可以用这个方法解决,比如抛出ReferenceError:Cannotaccess'__vite_ssr_import_1__'beforeinitialization,参考issue:https://github.com/vitest-dev/vitest/issues/1084。jest的isolateModules模块替代了这个api,这个api在jest中其实是比较冷门的,因为jest其实是全局共享一些变量实例的。比如有些模块需要引入mocks,实际上会是一个测试文件中的多个测试。case是共享的,所以如果你想让它们不共享,一般在玩笑中使用isolateModules来隔离这些模块的导入//xxx.test.tsdescribe('test-case',()=>{letmod:typeofimport('../src/test-case');beforeEach(async()=>{jest.isolateModule(()=>{mod=require('../src/test-case');})})})在vitest中,由于esmfirst的特性,其文件之间的实例共享是单独隔离的。如果需要在文件中隔离这样的模块importingmock,可以使用vi.resetModules()这个方法也需要将jest中的requiremoduleimport修改为动态importimport(ESMfirst)://xxx.test。tsdescribe('test-case',()=>{letmod:typeofimport('../src/test-case');beforeEach(async()=>{vi.resetModules();mod=awaitimport('../src/test-case');})})这样其实你就可以解决问题了,也可以参考vitest核心贡献者的建议:总结一般来说,如果你想在你的新项目中使用vitest或者迁移老项目从jest到vitest的测试方案,我觉得可以从以下一方面入手:拥抱原生esm,熟悉相应的migration官方文档。参考vite仓库的使用(单元测试和e2e测试的使用)。以上仍然来自esbuild高性能,如果jest使用swc-jest预设配置来转换文件,在性能上未必会输vitest很多,但vitest只是从配置的简单性和一些现代工具(如TS、JSX、ESM)开箱即用本质上比臃肿的玩笑更灵活。虽然vitest还处于早期迭代阶段,但由于vite本身的使用以及社区中一些流行框架的使用,笔者认为vitest本身已经具备了在实际项目中使用的能力。欢迎长按图片添加ssh为好友,第一时间为大家分享前端行业动态、学习路径等。与你共度2022!