From Jest to Vitest and back
Recently, I have remembered that there is a tool called Vitest, which supposedly should be blazing fast. Something like Vite vs Webpack. I spent a whole day to migrate and make everything work, figure how Vitest can be integrated with Angular. Just to discover that Jest is faster. Way faster.
Migration to Vitest
In general, migration looks pretty much simple thanks to the Analog.js project which provides a plugin for Vitest that allows it to work with Angular in a way very similar to how it worked in Jest.
For that, you need to install the package:
npm install @analogjs/platform --save-dev
And run the schematics (for NX workspace replace ng
with nx
):
ng g @analogjs/platform:setup-vitest --project [your-project-name]
After that, you can do a global replacement of jest
for vi
in *.spec.ts
. Since the API is 99% the same, it will just work. It’s not as simple, however, with types (e.g. SpyInstance etc.). They have different names, and you will have to find analogs in Vitest yourself. In general, this is not a problem because Vitest does not run type checks by default, so your TypeScript complaints are for the IDE only (unless you opt in for type checking).
One key thing to remember here is that during Vitest setup your config will have
globals: true
which:
1. Enables the use ofexpect
,it
, etc. without explicit imports.
2. Enables you to runfakeAsync
because under the hood the plugin monkey patches global APIs to work with Angular zones. This means that you cannot importdescribe
andit
fromvitest
and usefakeAsync
inside them as imported functions are not patched and you will get errors.
Now, you should be able to run ng test
, or simply vitest
to run all tests. It’s quite possible that you may encounter some errors due to missing global variables (which you may have configured for Jest previously).
When I first migrated, I started having errors around it.each
and fakeAsync
. A little of Googling has shown that it’s a known bug in the Analog.js plugin for Vitest. This led me to contributing to the project to fix the errors that were stopping me from migrating. I was surprised by how fast my changes were accepted and released. Unseen speeds.
Another problem I encountered was segfault errors. Some time later I discovered that the issue was caused by the use of vmThreads
for running tests in parallel. vmThreads
, according to the Vitest docs, are experimental and prone to memory leaks. The reason why it was chosen as default in the plugin is to make the new environment closer to what it used to be with Jest + JSDOM (read more about it here).
To fix this issue, I had to change the config to use threads
.
export default defineConfig(({ mode }) => {
return {
test: {
pool: 'threads', // add this property
},
};
});
Comparing performance
I compared the performance on a project with 500 tests. The tests are “classic” Angular production tests that avoid TestBed at all cost in favor of true unit tests. A few tests had to use TestBed to test effects as they require change detection to be triggered.
A discovery for me was that I did not need to create a component, or a host component (e.g. to test effects in services). Instead, set up TestBed to have all providers you need. Then, use
TestBed.runInInjectionContext(() => { service = new Service() })
to create an instance of a service or a component that has access to the effects’ scheduler. When you need to trigger effects, useTestBed.flushEffects()
. Easy peasy!.. I used to create a host component and callfixture.detectChanges()
to trigger effects 🥲
When I ran the tests on my MacBook Pro on M1 chip with 32 Gb with Jest, it took approximately 60 seconds to run all 500 tests with no cache. With Vitest, it took 90 seconds. A 50% increase in execution time.
Things had gotten even worse in CI/CD. With Jest it took approximately 90 seconds to run all tests. With Vitest, it took 200! Moreover, our GitLab pipelines often killed the job, even if we requested 7 Gb of RAM (we did not use vmThreads
by the time, so memory leaks shouldn’t have been the case). So, not only did it take drastically longer, but also we had to restart the job even more often.
Conclusion
It’s a little bit frustrating that spending almost a whole day brought performance degradations where improvements were expected.
Some say that the performance is worse because Vitest uses VM from Node.js, which is experimental now and has worse performance. And this may change in the future as it becomes more adopted and polished.
The migration from Jest to Vitest is very smooth already, and I expect it to become even smoother with time and adoption.
Also, one of the cool benefits I could see right away is that vi.mock
(which works the same way as jest.mock
) mocks ES modules as effortlessly as Jest did with CommonJS (no need for nasty workarounds with dynamic imports to work in ESM).
To use Vitest or not is entirely up to you. Right now, I don’t see any benefits of using Vitest. The tool seems to be not as adopted in the Angular community, the performance is worse. The VM for mocking ES Modules in Node.js is still experimental and not optimized enough.