Vitest vs. Jest
speakeasy.com
speakeasy.com
It's an excellent test runner and much, much faster than the others, especially if you set the "isolation" flag to "none".
Even when testing libraries requiring node in which case I'll run the tests with node:test but still use bun's built-in bundler [1] to build & bundle the typescript code base.
Basically Bun's my first choice, but will still use node if I have to.
If you want the extra confidence, run it with bun:test and node:test to cover JSC and V8. But JS should be JS and for unit tests feeling a need to test on multiple JS engines is a possible code smell.
The point about saying "those aren't unit tests" is that it is a "wrong tool for the wrong job" problem. Of course I don't expect Node or Bun to act like any specific browser. The things I want to test in Node or Bun are "universals" that should work in any JS environment, no matter what. If I need to catch V8-specific bugs or JSC-specific bugs, I want to use a smarter integration testing tool like Cypress or Playwright for that, not slow down my unit test suite trying to make a unit test harness pretend to be browsers.
That said, there are a lot more issues than just the differences between JSC and V8 - the Bun and NodeJS runtimes, while theoretically exposing the same modules, have a lot of practical differences that are visible in userland code, such as different exceptions, different orderings of queue events, etc. I imagine quite a lot of more complex code depends in some way on platform behaviours like these.
But if you are writing for the browser the hope is that it shouldn't matter if you test with bun:test or node:test. On Node you will have to "polyfill"/"test mock" a few more browser APIs than with Bun (which tries to follow the browser APIs a bit closer), but those are pretty well known today. I feel like the choice in that case between bun:test and node:test should be whatever is faster/more productive and easier-to-hand, not based on "Bun is more like Safari because it uses JSC" and "Node is more like Chromium because it uses V8". I don't think that difference should matter when choosing a unit test tool. The platform standards should be the standards and the best stuff to unit test shouldn't (in theory) be browser specific and in practice you generally want to be able to "run anywhere". I'd reach to something like Cypress or Playwright for tests that are browser specific and whichever was closest to hand or easiest to use in the moment of bun:test, node:test, or deno:test for the speediest unit test suite.
(But also, some of my strong opinions here also include that I'm among those afraid of the Chromium hegemony/monoculture and developers today only testing browser code in a V8 swamp, so yeah "you don't need to test in Bun unless you are worried about Safari testing" concerns me as an implied attitude above.)
The only exception I can think of is proper tail calls, which Google explicitly removed and I don't know if Mozilla ever tried to implement: https://compat-table.github.io/compat-table/es6/#test-proper... https://chromestatus.com/feature/5516876633341952
I switched to Vitest because Jest was giving me flaky coverage results: https://github.com/nodejs/node/issues/51251
And while Vitest' coverage is stable, I had to add an indirection layer to change how I mock out calls to GitHub's API (@github/octokit).
Oh, side note: vite-node's --watch functionality does not restart the process. It does hot reloading which is different enough that it requires some rework at how you set up your app structure and callbacks.
I also would argue Jest development slowed down after Meta transferred the project to OpenJS, so…
https://news.ycombinator.com/newsguidelines.html
A rotten apple doesn't roll far from the tree. The vast majority of Meta projects and former projects are garbage. If you need to ask why, because it's experience.
Most of the migration effort was spent on configuration for our needs. Vitest is quite flexible so it was just a matter of nailing down what worked for us.
More generally this is one of the really annoying things about Node.js. You might have some circular dependency that works everywhere, but it fails when you use some specific bundler or test runner. Cause they all handle things slightly differently.
I've used p3rl.org/circular::require on perl projects before now to root out such dependencies and kill them with fire, and "figure out how to get equivalent technology in TS/JS codebases" is on my list of things to do.
Fundamentally, a circular dependency that currently (apparently) "works everywhere" should still be treated as an unexploded footbomb.
Excising them can absolutely be annoying and inconvenient, but IME not nearly as much as having the damn thing go off when a future maintainer makes an innocuous change an indeterminate amount of time later.
tl;dr: HUMBUG (and sorry I don't have a better answer yet)
We are not using the module mocks from Jest in our project and a quick PoC showed significant (3x) speed improvement, but i am worried about the battle-tested:ness.
node --experimental-transform-types --experimental-test-module-mocks --env-file test/test.env --test
It then automatically runs all my test/*.tests.ts files.
So nice to get rid of all that extra config. As this is still a tiny project, I can tolerate some API changes in these experimental features.
Jest is full of unneeded magic (= complexity), whereas node:test is straightforward and has a better design. Highly recommended. You can turn off process isolation for extra speedup!
Without process isolation we got 6x speedup vs. jest! But it is difficult to ensure a clean slate between our test suites (~40 suites which all use a test DB)
We are also using some helpers to mock, e.g https://github.com/marchaos/jest-mock-extended - its such helper libraries im concerned about.
We're working with a relational db, and we're DELETE FROM all tables that we use at the beginning of each test. That's fast enough so far. Process isolation doesn't help you with state in out-of-process dependencies anyway.
AFAIK JUnit and other test runners don't do any process isolation for unit tests. Why is JavaScript so different?
If I could just make some rules that all engineers would follow exactly, I'd simply make a rule "write no bugs" and then I wouldn't need a test framework at all.
Maybe if you are on Node 23 and you absolutely have everything you need, then node:test is okay, but I DO NOT recommend it.
Ended up going with playwright for the “batteries included” browser support and because we use .Net Core for the backend.
dotnet doesn’t always get the best support from pure JS/TS projects, so it seemed like a reasonable hedge.
But I'd tend to have relatively independent test suites for my frontend model/state layer and the "how it behaves live in a browser" stuff because that way if I'm not currently iterating on my view components I can test the rest of my frontend code changes without having to faff with headless browser instances.
(obviously you'd then want to run both before committing, and again under CI before merge, but still)
Depending on how thin your model/state layer is (and if you have a Sufficiently Rich Backend then it can probably be very thin without that being an issue) your situation may not match mine, of course.
But y'know, free data point, worth exactly what you paid :D