|
| 1 | +--- |
| 2 | +draft: true |
| 3 | +title: Why Vitest? |
| 4 | +slug: /angular/why-vitest |
| 5 | +--- |
| 6 | + |
| 7 | +# Why Vitest? |
| 8 | + |
| 9 | +There are many test runners and testing frameworks available in the web ecosystem, but let's focus on those that are most relevant to Angular developers. |
| 10 | + |
| 11 | +Since the deprecation of [Karma](https://karma-runner.github.io/latest/index.html) in April 2023, the main alternatives in the Angular ecosystem are _(by alphabetical order)_: |
| 12 | + |
| 13 | +- [Jest](https://jestjs.io/) |
| 14 | +- [Vitest](https://vitest.dev/) |
| 15 | +- [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/) (_While promising, Web Test Runner is still in its early stages so we will not consider it in this comparison._) |
| 16 | + |
| 17 | +While both Jest and Vitest are mature and widely used in the web ecosystem, Jest has been massively adopted by the Angular community for many years. However, **Vitest is gaining traction and is fixing many of the pain points that Angular developers have been facing with Jest**. |
| 18 | + |
| 19 | +Let's explore some of the reasons why you should consider using Vitest for your Angular projects. |
| 20 | + |
| 21 | +## 📦 ESM Support |
| 22 | + |
| 23 | +Vitest was design with ECMAScript modules (ESM) compatibility in mind. This means that Vitest supports ESM out of the box. This is a big deal because Angular and the whole JavaScript ecosystem is moving towards ESM. |
| 24 | + |
| 25 | +Beyond all the intrinsic advantages of ESM, **this means that Vitest does not require any configuration to downgrade ESM to CommonJS**, even for third-party libraries. |
| 26 | + |
| 27 | +Haven't you ever been annoyed by the common `SyntaxError: Unexpected token 'export'` that you get when you forget to downgrade ESM to CommonJS in Jest, and the weird negative regex in [`transformIgnorePatterns`](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)? |
| 28 | + |
| 29 | +- https://stackoverflow.com/questions/42260218/jest-setup-syntaxerror-unexpected-token-export |
| 30 | +- https://stackoverflow.com/questions/49263429/jest-gives-an-error-syntaxerror-unexpected-token-export |
| 31 | +- https://github.com/aws-amplify/amplify-js/issues/11435 |
| 32 | +- https://github.com/fullcalendar/fullcalendar/issues/7113 |
| 33 | + |
| 34 | +```js |
| 35 | +transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)']; |
| 36 | +``` |
| 37 | + |
| 38 | +:::warning |
| 39 | +This also means that Jest "transform" phase will be slower than Vitest's because Jest will have to transform third-party libraries from ESM to CommonJS. |
| 40 | +::: |
| 41 | + |
| 42 | +:::tip What about Vitest? |
| 43 | +**With Vitest, you won't have to deal with that anymore.** |
| 44 | +::: |
| 45 | + |
| 46 | +### Isn't Jest also moving towards ESM? |
| 47 | + |
| 48 | +[**Jest ESM support is still experimental**](https://jestjs.io/docs/ecmascript-modules) as it relies on an experimental Node.js feature. |
| 49 | + |
| 50 | +More precisely, Jest isolation model relies on Node.js [V8 Virtual Machines _(VM)_](https://nodejs.org/api/vm.html#vm-executing-javascript) where [ESM support is still experimental](https://nodejs.org/api/vm.html#vm_class_vm_module). |
| 51 | + |
| 52 | +## 🏝️ Jest vs. Vitest Isolation Modes |
| 53 | + |
| 54 | +### Jest Isolation Mode |
| 55 | + |
| 56 | +**Jest has only a single isolation mode: VM**. |
| 57 | + |
| 58 | +This means that Jest runs each test file in a separate Node.js VM. |
| 59 | +Theoretically, this is the best trade-off between isolation and performance. However, in practice, this comes with a cost: |
| 60 | + |
| 61 | +- As mentioned before, ESM support in VMs is still experimental in NodeJS. |
| 62 | +- Conceptual memory leaks as third-party libraries can be reloaded in each VM, thus causing memory leaks to amplify: |
| 63 | + - https://chanind.github.io/javascript/2019/10/12/jest-tests-memory-leak.html |
| 64 | + - https://github.com/jestjs/jest/issues/6814 |
| 65 | + - https://github.com/jestjs/jest/issues/14605 |
| 66 | + - https://github.com/jestjs/jest/issues/7874 |
| 67 | + |
| 68 | +### Vitest Isolation Modes |
| 69 | + |
| 70 | +Conversely, **Vitest offers [multiple isolation modes](https://vitest.dev/guide/improving-performance#test-isolation)**: |
| 71 | + |
| 72 | +- **VM**: Each test file runs in a separate VM. _(just like Jest)_ |
| 73 | +- **Threads**: Each test file runs in a separate thread. |
| 74 | +- **Forks**: Each test file runs in a separate process. |
| 75 | +- **No Isolate**: All test files run in the same pool of threads or forks. |
| 76 | + |
| 77 | +:::tip Choose the isolation mode that best fits your needs with Vitest |
| 78 | +This means that with Vitest you can where you want to put the cursor between isolation and performance. |
| 79 | +::: |
| 80 | + |
| 81 | +In other words: |
| 82 | + |
| 83 | +- **for most Angular tests you can disable the isolation in Vitest** to get the best performance. Angular's [`TestBed`](https://angular.dev/api/core/testing/TestBed?tab=api) provides a sufficient isolation mechanism. |
| 84 | +- **for tests that require a higher level of isolation**, you can use `forks` isolation mode. This is the slowest but the most isolated mode. You can even make calls to `chdir` as this will only change the current directory for the process of the test file where this call is performed. |
| 85 | + |
| 86 | +:::note |
| 87 | +`TestBed` isolation mechanism and Angular's abstractions proved their efficiency with Karma. In fact, Karma runs all the tests in the same browser window. |
| 88 | +::: |
| 89 | + |
| 90 | +## ⚡️ Performance |
| 91 | + |
| 92 | +When it comes to testing, the speed of feedback is crucial. You don't want to start multitasking while waiting for your tests to complete, or worse, to run them less frequently because they are too slow, right? |
| 93 | + |
| 94 | +:::warning |
| 95 | +Please note that the following performance comparison is based on a "lab" situation and may not reflect your specific use case. You should always run your own benchmarks to make an informed decision. |
| 96 | +::: |
| 97 | + |
| 98 | +This benchmark compares the performance of Jest with Vitest and its different isolation modes: |
| 99 | + |
| 100 | +- `vitest-threads-no-isolate`: Vitest with threads and no isolation between different test files running in the same thread. |
| 101 | +- `vitest-vmforks`: Vitest with fork workers and VM isolation between different test files running in the same process. |
| 102 | +- `vitest-threads`: Vitest with thread workers running each test file in a separate thread. |
| 103 | +- `vitest-forks`: Vitest with fork workers running each test file in a separate process. |
| 104 | + |
| 105 | +_💻 The source code of the benchmark is available [here](https://github.com/yjaaidi/experiments/tree/angular-testing-benchmark)._ |
| 106 | + |
| 107 | +_📊 The last benchmark run results are available [here](https://github.com/yjaaidi/experiments/actions/workflows/benchmark.yml). (The disparity is even more significant when running the benchmark on Github Actions.)_ |
| 108 | + |
| 109 | +### Benchmark Cold Start |
| 110 | + |
| 111 | +In this benchmark, Jest's and Angular's caches are cleared before each run to simulate a cold start. |
| 112 | + |
| 113 | +| Command | Mean [s] | Min [s] | Max [s] | Relative | |
| 114 | +| :----------------------------- | ------------: | ------: | ------: | ----------: | |
| 115 | +| 🥇 `vitest-threads-no-isolate` | 2.925 ± 0.084 | 2.801 | 3.039 | 1.00 | |
| 116 | +| 🥈 `vitest-vmforks` | 4.761 ± 0.172 | 4.565 | 5.116 | 1.63 ± 0.08 | |
| 117 | +| 🥉 `vitest-threads` | 7.795 ± 0.356 | 7.549 | 8.771 | 2.67 ± 0.14 | |
| 118 | +| `jest` | 8.282 ± 0.253 | 7.995 | 8.641 | 2.83 ± 0.12 | |
| 119 | +| `vitest-forks` | 9.362 ± 0.259 | 8.999 | 9.792 | 3.20 ± 0.13 | |
| 120 | + |
| 121 | +:::tip Conclusion |
| 122 | +**Vitest can be up to **3x faster** than Jest.** |
| 123 | +::: |
| 124 | + |
| 125 | +### Benchmark Warm Start |
| 126 | + |
| 127 | +In this benchmark, Jest's and Angular's caches are kept between runs to simulate a warm start. |
| 128 | + |
| 129 | +| Command | Mean [s] | Min [s] | Max [s] | Relative | |
| 130 | +| :----------------------------- | ------------: | ------: | ------: | ----------: | |
| 131 | +| 🥇 `vitest-threads-no-isolate` | 2.945 ± 0.048 | 2.906 | 3.075 | 1.00 | |
| 132 | +| 🥈 `vitest-vmforks` | 4.768 ± 0.092 | 4.594 | 4.962 | 1.62 ± 0.04 | |
| 133 | +| 🥉 `jest` | 4.775 ± 0.119 | 4.627 | 5.065 | 1.62 ± 0.05 | |
| 134 | +| `vitest-threads` | 7.581 ± 0.201 | 7.299 | 7.908 | 2.57 ± 0.08 | |
| 135 | +| `vitest-forks` | 9.172 ± 0.131 | 8.930 | 9.385 | 3.11 ± 0.07 | |
| 136 | +| `angular-cli-web-test-runner` | 9.980 ± 0.074 | 9.828 | 10.095 | 3.39 ± 0.06 | |
| 137 | + |
| 138 | +:::tip Conclusion |
| 139 | +During the warm start, Jest leverages its cache to speed up the test execution. |
| 140 | +In this case, Jest is as fast as Vitest with similar isolation mode but still 40% slower than Vitest without isolation. |
| 141 | + |
| 142 | +As of today, Vitest does not have a cache mechanism, but the [Angular vite plugin could leverage Angular's cache](https://github.com/analogjs/analog/pull/1443) to speed up the test execution. |
| 143 | +::: |
| 144 | + |
| 145 | +### Watch Mode |
| 146 | + |
| 147 | +:::info |
| 148 | +Note that the current benchmark does not measure the `watch` mode where Vitest is known to shine with **5x to 10x faster feedback than Jest**. |
| 149 | + |
| 150 | +This is decisive for Test-Driven Development _(TDD)_. 😇 |
| 151 | +::: |
| 152 | + |
| 153 | +## 📝 Unified Configuration with Vite |
| 154 | + |
| 155 | +Vitest can reuse the [Vite](https://vitejs.dev/) configuration. This means that whenever something is fixed or improved for the Vite dev server, you will be able to benefit from it in Vitest as well. |
| 156 | +For instance, an official Angular plugin for Vite could partially reuse the [Vite configuration](https://github.com/angular/angular-cli/blob/4d437ec1930016ce972fc365bbb71d0607124e83/packages/angular/build/src/builders/dev-server/vite-server.ts) for Vitest. |
| 157 | + |
| 158 | +## 🔮 Vitest Is Evolving Fast |
| 159 | + |
| 160 | +While the Jest team is still [doing a great job](https://github.com/jestjs/jest/pulse/monthly), [Vitest is evolving faster](https://github.com/vitest-dev/vitest/pulse/monthly). |
| 161 | + |
| 162 | +## 💻 Enhanced API |
| 163 | + |
| 164 | +While Vitest covers the same features and APIs as Jest, making migration easier, Vitest also provides many additional APIs. |
| 165 | + |
| 166 | +Here is a non-exhaustive list: |
| 167 | + |
| 168 | +- [`expect.soft`](https://vitest.dev/api/expect.html#soft) that comes in handy when making multiple assertions in a single test. |
| 169 | +- [`expect.poll`](https://vitest.dev/api/expect.html#poll) and [`vi.waitFor`](https://vitest.dev/api/vi.html#vi-waitfor) for retrying assertions. Note that retriability is one of the key features that made [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/) successful. |
| 170 | +- [`stubGlobal`](https://vitest.dev/api/vi.html#vi-stubglobal) and [`stubEnv`](https://vitest.dev/api/vi.html#vi-stubenv) to override globals and env instead of handling them manually. _(It is better to avoid this by providing test doubles at an abstraction level you control but that's another story we'll cover later.)_ |
| 171 | +- [Fixtures](https://vitest.dev/guide/test-context.html#extend-test-context) just like Playwright. |
| 172 | + |
| 173 | +## 🍱 Vitest is a Feature Buffet |
| 174 | + |
| 175 | +### Type Testing |
| 176 | + |
| 177 | +Vitest supports [type testing](https://vitest.dev/guide/testing-types.html) out of the box. This means that intead of compilation errors using TypeScript's [`@ts-expect-error`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#-ts-expect-error-comments), you can write type tests for your complex generics, conditional types, and more using a similar API to your regular tests. |
| 178 | + |
| 179 | +```ts |
| 180 | +test('create a serializer for the provided type', () => { |
| 181 | + const serializerFn = createSerializer<User>(); |
| 182 | + expectTypeOf(serializerFn).parameter(0).toEqualTypeOf<User>(); |
| 183 | + expectTypeOf(serializerFn).returns.toEqualTypeOf<string>(); |
| 184 | +}); |
| 185 | +``` |
| 186 | + |
| 187 | +This will also enhance your developer experience by providing an explicit Vitest test report for your type tests. |
| 188 | + |
| 189 | +```sh |
| 190 | +// highlight-next-line |
| 191 | +FAIL serializer.spec-d.ts > create a serializer with the provided type |
| 192 | +TypeCheckError: Type 'string' does not satisfy the constraint '"Expected string, Actual void"'. |
| 193 | +❯ serializer.spec-d.ts:6:52 |
| 194 | + 4| const serializerFn = createSerializer<User>(); |
| 195 | + 5| expectTypeOf(serializerFn).parameter(0).toEqualTypeOf<User>(); |
| 196 | + 6| expectTypeOf(serializerFn).returns.toEqualTypeOf<string>(); |
| 197 | + | ^ |
| 198 | + 7| }); |
| 199 | +``` |
| 200 | + |
| 201 | +### UI |
| 202 | + |
| 203 | +Vitest provides a [UI](https://vitest.dev/guide/ui.html) to visualize your tests, their results, and their performance. |
| 204 | + |
| 205 | +### Module Graph |
| 206 | + |
| 207 | +What's even more compelling is that Vitest's UI provides a [module graph](https://vitest.dev/guide/graph.html) to visualize the dependencies between the modules loaded by your tests. |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | +### Test Duration Report |
| 212 | + |
| 213 | +While this might seem futile, Vitest provides a detailed test duration report. This is very useful to quickly identify the performance bottleneck. |
| 214 | + |
| 215 | +``` |
| 216 | +Duration 2.77s (transform 952ms, setup 195ms, collect 1.02s, tests 802ms, environment 390ms, prepare 38ms) |
| 217 | +``` |
| 218 | + |
| 219 | +- `prepare`: time spent preparing the test runner _(e.g. this will take much more time with the `forks` isolation mode compared to `no-isolate` mode)_. |
| 220 | +- `transform`: time spent transforming the test files. |
| 221 | +- `environment`: time spent setting up the test environment _(e.g. `happy-dom` or `jsdom` setup)_. |
| 222 | +- `setup`: time spent executing the setup files. |
| 223 | +- `collect`: time spent collecting the tests _(e.g. this includes the execution time of your test files code that is outside a `test`)_. |
| 224 | +- `tests`: time spent executing the tests. |
| 225 | + |
| 226 | +:::note |
| 227 | +Note that the total duration is often lower than the sum of the parts due to parallelization. |
| 228 | +::: |
| 229 | + |
| 230 | +### Benchmarking _(experimental)_ |
| 231 | + |
| 232 | +Vitest also supports [benchmarking](https://vitest.dev/guide/features.html#benchmarking) out of the box. This means that you can write benchmarks for your functions and compare their performance over time. |
| 233 | + |
| 234 | +### Browser mode _(experimental)_ |
| 235 | + |
| 236 | +This is one of the latest sweets from the Vitest buffet. [Vitest can run your tests in a real browser](https://vitest.dev/guide/browser/). |
| 237 | + |
| 238 | +It is still experimental so it is still early to draw conclusions, but right now, we think that [Playwright Component Testing](https://github.com/jscutlery/devkit/tree/main/packages/playwright-ct-angular) approach is more suitable. We will elaborate on this in a future chapter. |
0 commit comments