Is Bun Ready for Unit Testing in 2024?

Is Bun Ready for Unit Testing in 2024?

Published:

Bun comes with a built-in test runner. Is this a serious alternative to established runners like Jest or Vitest? Let’s find out.

Cover image

When rebuilding my blog, I wanted to unit-test my content backend service. Of course, my first thought was to use Vitest because I have had a good experience in the past. But I could not resist my curiosity when I found out that Bun comes with its own builtin test runner. So I gave it a try.

I will share my experience and thoughts about B un test with you in this article.

Why another test runner?

You may be familiar with other test runners like Vitest or Jest and ask: “Why should I care about another one?”

The short answer to this is: Performance.

If the tests within a project are too slow, they become painful for the developers to execute. Moreover, the CI pipeline will take longer and the devs get bored by waiting for its feedback on their changes. In the end, slow tests slow down the whole development process. This is expensive.

The software quality might suffer as well, because when executing tests has high friction during development, it can quickly result in less tests being written.

So quick test execution time is significant and the choice of your test runner has a high impact on this.

How does Bun test perform?

The Bun test runner is extremely fast. According to a benchmark from the docs, Bun runs 266 React SSR tests faster than Jest can print its version number. I also noticed that performance is not an issue at all with my current test suite of my content service. The 11 test files with 56 tests run within 0.25 seconds on my M1 mac.

The watch mode is also very enjoyable. It uses Hot Module Replacement (HMR) like Vitest and re-executes only those tests that are dependent on the modules you just touched, within the blink of an eye.

What does writing tests in Bun feel like?

Bun test aims to be very close to Jest’s API. The same is true for Vitest. So, if you are used to one of those runners, testing with Bun test will be quite familiar. Here is what a basic test can look like:

import { describe, expect, it } from "bun:test";

describe("true", () => {
  it("should be truthy", () => {
    expect(true).toBeTruthy();
  });
});

Like in Vitest, we import our test helpers from a module, instead of having them globally exposed.

An important part of unit testing is mocking of functions and modules. This is also enabled by Bun test.

import { describe, expect, it, mock } from "bun:test";
import { getVersion } from "./version";

const mockReadFile = mock(async () =>
  JSON.stringify({ version: "1.2.3" }),
);

mock.module("fs", () => ({
  promises: { readFile: mockReadFile },
}));

describe("version()", () => {
  it("should return the version", async () => {
    const version = await getVersion();

    expect(mockReadFile).toHaveBeenCalledWith(
      "package.json",
      "utf-8",
    );
    expect(version).toBe("1.2.3");
  });
});

While it looks very similar to mocking in Jest or Vitest, there is no hoisting in Bun tests. The module cache is instead patched at runtime and the module member bindings are updated accordingly. This lets us easily close over mocks within the module mocking factories, which can be confusing at times in the other test runners.

Is it ready to use?

So, given the great performance boost, we all should switch to Bun test. Right?

Not necessarily. There are things that may hold us back.

Feature completeness

There are many matchers and features from Jest and Vitest already implemented . Some essential ones are missing, though.

I.e., at the time of writing, the entire fake timers features are not yet implemented. This can easily be a dealbreaker for certain tests.

So if we depend on those missing features, we can either find a way to work around them or split our tests to run some of them with another test runner.

A solid way to partition your test files is to give them different extensions. This also works well, if you have your tests next to the implementation, scattered all over the place.

In Vitest and Jest, a different extension can easily be configured. Here is an example of a vitest.config.ts file.

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["src/**/*.vspec.ts"],
  },
});

Now we can run the Bun tests via bun test , whereas the Vitest tests are run via bun vitest .

With each Bun upgrade, you can check whether you can migrate some tests to the Bun test runner.

Test isolation

When you write tests in Bun, they are not running isolated. This means that if you have side effects within one test suite, those might affect the tests within a subsequently executed suite.

So if you mock a module in suite A, the mock will be present in suite B as well. When mocking modules, it is patched in-place, so you cannot simply restore the actual module.

This may not be an issue, if you never mock your own code and stay on one test level. But it can lead to issues, if you want to have a module mocked in one case, and in the other case you wish to use the original implementation. Typically, this conflict happens when you mix up test levels like unit and integration tests. So this should be avoided within a single test run.

Test isolation does not come without performance cost, so even if this feature may be shipped in the future, you might consider leaving it switched off, if the penalty is too high.

Code coverage

Bun test comes with code coverage reporting . At the time of writing, this is very basic. It only prints to the command line and is limited to display line and function coverage.

If you need more detailed reporting or even a machine-readable format that can be processed by your CI pipeline, you will have to stick to another runner for now.

IDE integration

Currently, there is no IDE integration for running Bun tests. I didn’t really miss it until now, but from experience in larger projects, I can tell that being able to debug tests can be quite useful in some cases. Until then, we can rely on good old console.log debugging.

Conclusion

On the one hand, Bun tests are very fast and for basic tests, it is well-equipped. On the other hand, for some of us, there are some potential show-stoppers for using Bun test for now.

If your only concern are some missing features, you can consider partitioning your tests and use a second test runner to close for this gap.

All in all, the Bun team does a fantastic job, developing a fast test runner for the JS community.

Never stop learning and happy coding!