{"id":500,"date":"2026-07-24T13:02:41","date_gmt":"2026-07-24T13:02:41","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/vitest-vs-jest-2026-the-day-i-stopped-fighting-my-test-runner\/"},"modified":"2026-07-24T13:02:41","modified_gmt":"2026-07-24T13:02:41","slug":"vitest-vs-jest-2026-the-day-i-stopped-fighting-my-test-runner","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/vitest-vs-jest-2026-the-day-i-stopped-fighting-my-test-runner\/","title":{"rendered":"Vitest vs Jest in 2026: The Day I Stopped Fighting My Test Runner"},"content":{"rendered":"<p>Confession: I was the guy who defended Jest at every code review for about three years. Someone would open a PR that added Vitest to a side project and I&rsquo;d leave a comment like &ldquo;why add a dependency when Jest already works?&rdquo; I genuinely believed it. Then last spring I spent a full Saturday afternoon fighting a Jest ESM config on a real monorepo, gave up, ran one migration command, and had the whole suite green on Vitest before dinner. That afternoon changed my mind, and this post is me trying to be honest about why.<\/p>\n<p>I&rsquo;ll give you the short version up front, because I know some of you just want the answer: if you&rsquo;re on Vite already, or you keep hitting ESM pain in Jest, move to Vitest and don&rsquo;t overthink it. If you&rsquo;re on a big stable Create React App or Next.js codebase that already tests fine, there is no prize for switching. The rest of this is the reasoning, the actual config, and the two places where I still reach for Jest.<\/p>\n<h2 id=\"why-i-stuck-with-jest-for-so-long\">Why I stuck with Jest for so long<\/h2>\n<p>Jest earned its reputation. For most of the last decade it was the default, it came bundled with Create React App, and every Stack Overflow answer assumed you were running it. When something broke, the fix was usually the first result. That kind of gravity is worth a lot when you&rsquo;re shipping instead of tinkering.<\/p>\n<p>The other thing is that my tests were mostly fine. I had snapshots, I had a <code>jest.config.js<\/code> I&rsquo;d copy-pasted between projects for years, and it worked. Nobody rewrites a working test suite for fun. So I kept copying that config forward, project after project, and I stopped noticing how much of it was there to paper over one specific problem.<\/p>\n<p>That problem was modules.<\/p>\n<h2 id=\"the-esm-wall-i-kept-hitting\">The ESM wall I kept hitting<\/h2>\n<p>Here&rsquo;s the thing that finally wore me down. More and more of the libraries I depend on ship as pure ES modules now. Jest was built in a CommonJS world, and its ESM support is real but still flagged as experimental in the <a href=\"https:\/\/jestjs.io\/docs\/ecmascript-modules\" rel=\"nofollow noopener\" target=\"_blank\">official Jest docs<\/a>. In practice that means you end up with a pile of workarounds.<\/p>\n<p>My <code>jest.config.js<\/code> had slowly grown into this:<\/p>\n<pre><code class=\"language-js\">\/\/ jest.config.js \u2014 the accumulated scar tissue\nmodule.exports = {\n  preset: &quot;ts-jest\/presets\/default-esm&quot;,\n  extensionsToTreatAsEsm: [&quot;.ts&quot;, &quot;.tsx&quot;],\n  transform: {\n    &quot;^.+\\\\.tsx?$&quot;: [&quot;ts-jest&quot;, { useESM: true }],\n  },\n  transformIgnorePatterns: [\n    \/\/ half a dozen node_modules that ship ESM and Jest can't parse\n    &quot;node_modules\/(?!(nanoid|uuid|@my-org\/ui|lowdb|other-esm-pkg)\/)&quot;,\n  ],\n  moduleNameMapper: {\n    &quot;^(\\\\.{1,2}\/.*)\\\\.js$&quot;: &quot;$1&quot;,\n  },\n};\n<\/code><\/pre>\n<p>Every time I added a dependency that shipped ESM, the suite would blow up with <code>SyntaxError: Cannot use import statement outside a module<\/code>, and I&rsquo;d add another package to that <code>transformIgnorePatterns<\/code> regex. I did this at least a dozen times across projects. It&rsquo;s the kind of thing you stop questioning because it&rsquo;s always been broken, so it feels normal.<\/p>\n<p>The Saturday that broke me, I added one small ESM-only date library and spent two hours getting the regex right. At some point I opened a new terminal and typed <code>npm create vite<\/code> just to see how bad the alternative was. It was not bad.<\/p>\n<h2 id=\"what-the-vitest-migration-actually-looked-like\">What the Vitest migration actually looked like<\/h2>\n<p>Vitest runs your tests through Vite&rsquo;s own transform pipeline, so ESM is just how it works. There&rsquo;s no experimental flag and no <code>transformIgnorePatterns<\/code> graveyard. The <a href=\"https:\/\/vitest.dev\/\" rel=\"nofollow noopener\" target=\"_blank\">Vitest docs<\/a> are refreshingly blunt about this being the whole point.<\/p>\n<p>The migration guide is short enough that I read the entire thing, which almost never happens with tooling docs. Vitest ships a Jest-compatible API on purpose, so <code>describe<\/code>, <code>it<\/code>, <code>expect<\/code>, and most matchers are identical. Their <a href=\"https:\/\/vitest.dev\/guide\/migration.html\" rel=\"nofollow noopener\" target=\"_blank\">migration guide<\/a> walks through the handful of real differences, and honestly the handful is small.<\/p>\n<p>The config that replaced all that scar tissue above:<\/p>\n<pre><code class=\"language-ts\">\/\/ vitest.config.ts \u2014 the whole thing\nimport { defineConfig } from &quot;vitest\/config&quot;;\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: &quot;jsdom&quot;,\n    setupFiles: [&quot;.\/test\/setup.ts&quot;],\n  },\n});\n<\/code><\/pre>\n<p>That&rsquo;s it. No transform block, no ignore patterns, no module name mapper. The ESM libraries that Jest choked on just load.<\/p>\n<p>The test files barely changed. This one ran under both, and I only had to swap the import line:<\/p>\n<pre><code class=\"language-ts\">\/\/ user.test.ts\nimport { describe, it, expect } from &quot;vitest&quot;; \/\/ was: from &quot;@jest\/globals&quot;\nimport { formatName } from &quot;..\/src\/user&quot;;\n\ndescribe(&quot;formatName&quot;, () =&gt; {\n  it(&quot;handles a normal name&quot;, () =&gt; {\n    expect(formatName(&quot;qasim&quot;, &quot;abrar&quot;)).toBe(&quot;Qasim Abrar&quot;);\n  });\n\n  it(&quot;trims stray whitespace&quot;, () =&gt; {\n    expect(formatName(&quot;  ada  &quot;, &quot;lovelace&quot;)).toBe(&quot;Ada Lovelace&quot;);\n  });\n});\n<\/code><\/pre>\n<p>If you set <code>globals: true<\/code>, you don&rsquo;t even need that import, and existing Jest-style files that rely on global <code>describe<\/code> and <code>expect<\/code> run untouched. For a codebase with a few hundred tests, my actual migration was: install Vitest, delete the Jest config, add a ten-line <code>vitest.config.ts<\/code>, run <code>npx vitest<\/code>, fix maybe four mocking calls. The whole thing took an afternoon, and most of that was me being paranoid and re-reading diffs.<\/p>\n<h2 id=\"where-jest-still-wins\">Where Jest still wins<\/h2>\n<p>I don&rsquo;t want to sell you a clean story, because it isn&rsquo;t one. There are two situations where I still pick Jest without hesitating.<\/p>\n<p>The first is a large existing project that already tests cleanly, especially older Next.js or Create React App setups where the Jest integration is documented and boring. Boring is a feature in testing. If your suite is green and your team knows the tooling, migrating is pure risk with no reward, and I&rsquo;d spend that afternoon on something a user will actually notice.<\/p>\n<p>The second is heavy mocking of the module system itself. Jest&rsquo;s mocking has had years of edge cases beaten out of it. Vitest&rsquo;s <code>vi.mock<\/code> is very close and gets closer every release, but if your codebase leans hard on <code>jest.mock<\/code> with automatic mocking and deep module hoisting, budget real time to port it. That&rsquo;s the one area where my &ldquo;one afternoon&rdquo; story would have been a lie.<\/p>\n<p>There&rsquo;s also the ecosystem tail. A few niche Jest plugins and reporters don&rsquo;t have Vitest equivalents yet. It&rsquo;s rare, but check your specific setup before you promise your team a smooth move.<\/p>\n<h2 id=\"how-id-decide-today\">How I&rsquo;d decide today<\/h2>\n<p>My rule now is embarrassingly simple. If a project already uses Vite for its build, I use Vitest from the first test, because sharing one config and one transform pipeline removes a whole category of &ldquo;works in the app, breaks in tests&rdquo; bugs. If a project is on an older bundler and its Jest suite is healthy, I leave it alone until there&rsquo;s a real reason to touch it, and ESM pain usually becomes that reason on its own.<\/p>\n<p>The speed difference is real but oversold. On my mid-size suite Vitest&rsquo;s watch mode felt snappier, mostly because it reuses Vite&rsquo;s module graph, but if someone tells you it&rsquo;s ten times faster on every project, be skeptical. Measure your own suite. The bigger win for me was deleting config, not shaving seconds.<\/p>\n<p>Testing philosophy matters more than the runner anyway. I wrote about that from the end-to-end side in my post on <a href=\"https:\/\/abrarqasim.com\/blog\/playwright-vs-cypress-2026-the-day-i-ripped-out-cypress\" rel=\"noopener\">ripping Cypress out for Playwright<\/a>, and the same lesson applies here: the tool that fits your build and your team beats the tool that wins benchmarks. Most of the consulting work I do lands on exactly this kind of call, and you can see the sort of projects I take on over on my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">work page<\/a>.<\/p>\n<p>Here&rsquo;s the concrete thing to do this week. Open your <code>jest.config.js<\/code> and look at your <code>transformIgnorePatterns<\/code>. If that list has grown past two or three packages, that&rsquo;s your suite telling you it&rsquo;s fighting ESM. Spin up a throwaway branch, run <code>npm i -D vitest<\/code>, drop in the ten-line config above, and run <code>npx vitest<\/code> once. You&rsquo;ll know within twenty minutes whether Vitest fits, and worst case you delete the branch and you&rsquo;ve lost nothing but a coffee&rsquo;s worth of time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I defended Jest for three years, then moved a real monorepo to Vitest in one afternoon. Here is the ESM pain that pushed me, the config, and where Jest still wins.<\/p>\n","protected":false},"author":2,"featured_media":499,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I defended Jest for three years, then moved a real monorepo to Vitest in one afternoon. Here is the ESM pain that pushed me, the config, and where Jest still wins.","rank_math_focus_keyword":"vitest vs jest","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[165,197],"tags":[44,199,30,63,200,198],"class_list":["post-500","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript","category-testing","tag-javascript","tag-jest","tag-testing","tag-typescript","tag-vite","tag-vitest"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/500","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=500"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/500\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/499"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=500"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=500"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=500"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}