Back to Blog
·4 min read

Why I switched from complex test frameworks to Node.js built-in test runner

Node.js 18's built-in test runner eliminates dependencies and simplifies testing without sacrificing functionality.

AI Dev
testing
nodejs
tooling
productivity

Why I switched from complex test frameworks to Node.js built-in test runner

I ripped out Jest from my latest project and replaced it with Node.js's built-in test runner. No more 15-second startup times, no more configuration files, no more fighting with ESM imports. Just simple, fast tests that run exactly how I expect them to.

The breaking point came when I spent an entire afternoon trying to get Jest to properly handle TypeScript imports in a monorepo. The error messages were cryptic, the documentation was scattered across three different packages, and the solution required adding four more configuration files. I was debugging my test setup instead of writing actual tests.

What Node.js Test Runner Gets Right

Node.js 18 shipped with a built-in test runner that covers 90% of what most projects actually need. It's fast, has zero dependencies, and the API is refreshingly straightforward:

import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { calculateTax } from './tax-calculator.js';
 
describe('Tax Calculator', () => {
  test('calculates basic sales tax', () => {
    const result = calculateTax(100, 0.08);
    assert.equal(result, 8);
  });
 
  test('handles zero tax rate', () => {
    const result = calculateTax(100, 0);
    assert.equal(result, 0);
  });
});

That's it. No jest.config.js, no babel transforms, no module mapping. It just works.

The Performance Difference is Dramatic

My Jest test suite took 12 seconds to start running the first test. The same tests with Node.js test runner start in under 200ms. This is not just about raw speed -- it's about maintaining flow state. When tests are fast, you run them constantly. When they're slow, you batch them up and lose the tight feedback loop that makes TDD actually useful.

The difference becomes even more pronounced in CI. Our GitHub Actions now complete in 2 minutes instead of 8. That means faster deployments, quicker feedback on pull requests, and developers who are not waiting around for builds to finish.

What You Actually Lose

I'll be honest about the tradeoffs. Node.js test runner does not have everything Jest offers:

  • No built-in mocking -- You'll need to use a library like testdouble or write simple mocks manually
  • No snapshot testing -- Though I've found most snapshot tests are brittle anyway
  • Limited assertion library -- Node's assert module is basic, but you can use any assertion library you want
  • No code coverage -- You'll need to use c8 or nyc separately

For most projects, these limitations are features, not bugs. Simpler tools force you to write simpler tests, and simpler tests are more maintainable.

When to Stick with Jest

Node.js test runner is not right for every project. If you have a massive existing test suite with complex mocking requirements, migrating might not be worth it. If you're working in a team where everyone already knows Jest inside and out, the learning curve might outweigh the benefits.

But for new projects, especially smaller ones, the built-in test runner is a breath of fresh air. It reminds me why I love Node.js in the first place -- pragmatic tools that do one thing well without a lot of ceremony.

Making the Switch

The migration was surprisingly straightforward. Here's what I did:

# Remove Jest and related packages
npm uninstall jest @types/jest ts-jest
 
# Update package.json scripts
{
  "scripts": {
    "test": "node --test",
    "test:watch": "node --test --watch"
  }
}

I had to rewrite a few mocks and update some assertion syntax, but most tests converted with minimal changes. The biggest adjustment was mental -- remembering that I do not need to configure everything. The defaults just work.

The Bigger Picture

This switch reflects a broader trend I've noticed in my development practices. I'm gravitating toward tools that do less but do it reliably. Complex frameworks solve problems I do not actually have while creating new ones I definitely do have.

Node.js test runner might not be the most feature-rich option, but it's fast, stable, and ships with the runtime I'm already using. Sometimes boring technology is exactly what you need to get real work done.