Open-source project
testem/testem avatar
testem/testem

Testem: a browser test runner that keeps your specs in real engines

Test'em 'Scripts! A test runner that makes Javascript unit testing fun.

2,918 stars413 forksJavaScriptMIT

At a glance

What is it?
Testem is an MIT-licensed JavaScript test runner that executes your suites in real desktop browsers, Node, or headless Chrome, and it is framework-agnostic by design. The interesting question is whether its watch-driven TDD loop is worth the install, and where it stops being the right tool.
Who is it for?
Adopt Testem if your suite needs to execute inside actual browser engines, or if you want one runner across Jasmine, QUnit, Mocha and custom adapters without rewriting specs. Skip it if you only need headless Node unit tests, since Node's own runner covers that with no browser launcher to maintain.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Testem solves: specs that never touch a real engine

Most JavaScript test setups run in a simulated or stripped-down environment. Testem takes the opposite position. The README describes it as a JavaScript test runner that runs your tests in real desktop browsers, so your specs execute in the same browser engines and DOM your users get. That distinction matters most when the thing you are testing is not pure logic: layout reads, event ordering, form behaviour, or anything where a DOM shim and a browser disagree.

The second problem it addresses is framework churn. Testem is framework-agnostic, and the README lists adapters for Jasmine, QUnit and Mocha, with others reachable through custom test framework adapters. A team that has already written Jasmine specs and later wants to try QUnit does not have to change the runner underneath.

Who it is for is stated plainly: anyone running unit, integration, end-to-end style suites, or custom setups. The repository backs that up with an examples directory containing separate folders for jasmine_simple, mocha_chai_simple, qunit-style setups, node_example, node_tap_example, browserify, babel, coffeescript, eslint and browserstack. That spread is the clearest signal of intended audience: teams with heterogeneous test stacks rather than a single prescribed style.

How Testem works: a local server, a socket, and launchers

The mechanism visible in the repository is a small local web server plus a socket channel. The dependency list includes express, socket.io, compression, chokidar, commander, mustache and tap-parser. Read together, that points to the shape of the tool: Express serves a generated test page and the assets under public/, socket.io carries results from the browser back to the terminal, and tap-parser reads TAP output when the target is a Node process rather than a browser.

In development mode the flow is explicit in the README. You run the testem command, it prints a URL, and you open that URL in a real browser. The page reports 0/0 before any specs exist. As you add .js files, Testem picks them up, includes them, and reruns automatically. The README walks through exactly this: a hello_spec.js in Jasmine style fails first, then a hello.js implementation makes it pass.

The watching layer is chokidar v5. Testem watches the current working directory and applies include and ignore patterns to watcher events. Four config keys govern it: src_files as the main watch list, watch_files as an optional override, src_files_ignore for exclusions such as node_modules, and disable_watching to turn the watcher off entirely. That separation between what is served to the browser and what triggers a rerun is the part worth understanding before you tune a large project.

Installing Testem and running your first browser spec

Testem needs a supported Node.js runtime. The required range lives in package.json under engines and is currently ^20.19.0, ^22.12.0, ^24.0.0, or >= 26.0.0. Check that before anything else, because a mismatch will surface as an install or startup failure rather than a clear message.

The README recommends installing twice, once as a dev dependency so your project pins a version, and once globally so the testem command is always on your PATH.

bash
npm install testem --save-dev
npm install testem -g

There is a detail here that catches people out. When you run the global testem inside a project directory that has a local install, the CLI automatically re-runs the local copy so the version stays in sync with package.json. If you genuinely need the global binary only, set TESTEM_USE_GLOBAL=1.

With that done, the simplest first run is to start in an empty directory and invoke the command with no arguments.

bash
testem

Testem prints a URL. Open it in a real browser and you should see a 0/0 test count, meaning the page loaded and no specs are present yet. Write a spec file in that directory, save it, and the watcher should pick it up and rerun. The README's own example is a Jasmine spec named hello_spec.js that expects hello() to return 'hello world', followed by a hello.js that defines that function.

For continuous integration the command changes rather than the configuration.

bash
testem ci

The README notes that GitHub Actions is a common way to run this, typically with the Headless Chrome or Chromium launcher, and points at the project's own .github/workflows/ci.yml as a reference. To see every available flag, the README directs you to testem --help rather than listing them. The development-mode text interface has its own controls: ENTER runs the tests, q quits, and arrow keys move between browser tabs.

Where Testem gets awkward: watching, CI browsers and configuration sprawl

The file watcher is the most likely source of friction. The README is direct about this: on some setups, including Docker, network filesystems and VMs, native fs.watch can be flaky. Chokidar supports CHOKIDAR_USE_POLLING=1 to force polling and CHOKIDAR_INTERVAL to set the polling interval in milliseconds. Polling works, but it trades CPU and latency for reliability, and on a large tree that trade is noticeable. If your repository lives on a mounted volume, expect to set these.

CI is the second constraint, and it is a constraint of the approach rather than a bug. Running in real browsers means something has to launch and manage those browsers on the build machine. The README's CI section assumes a launcher such as Headless Chrome or Chromium. A container that has no browser installed will not run your suite, and the failure will look like a launcher problem, not a test problem. The repository carries scripts named ci:safari-smoke, ci:legacy-phantomjs and ci:legacy-ie-sauce-smoke, which hints at how much environment-specific plumbing browser coverage can accumulate.

Third, configuration surface. Testem is agnostic, and agnostic tools push decisions onto you. The repository ships both testem.js and testem.yml at the top level, plus a docs directory with a config_file reference and a browser_args reference. There is a custom_adapter example and a dynamic_config example. None of this is unreasonable, but a project that wants one blessed way to write and run tests will find the number of valid configurations larger than it wants. The README itself does not document rollback behaviour for a failed CI run, so do not expect guidance there.

How Testem differs from Node's built-in runner and from Karma

The closest comparison in the JavaScript ecosystem is Karma, which also drives real browsers through a local server and a socket connection. The difference is emphasis rather than category. Karma's identity is bound tightly to Angular's tooling lineage and to a plugin ecosystem that grew around it, while Testem's README frames the tool around two named use cases, test-driven development and continuous integration, and around the text interface you drive with ENTER and q in a terminal. If you want a terminal-centric TDD loop where the browser is a target you open once and leave running, Testem's design is aimed at exactly that. If you want a configuration object that a framework generator writes for you, the trade-off runs the other way.

The other alternative is Node's own test runner, and here the difference is categorical. Node's runner executes in Node. Testem can also target Node, and the repository includes node_example, node_tap_example and node_test folders, with tap-parser in the dependency list to read TAP output. But if every one of your tests is a pure Node unit test with no DOM, adding a browser launcher, a socket layer and a watch policy is overhead you do not need. Testem earns its keep when at least some of your suite has to run where the DOM actually exists.

Maintenance, upgrades and the MIT licence

The repository is not archived, and the last push was on 2026-09-14. Recent releases are v3.21.0 on 2026-09-07, v3.20.2 on 2026-08-26 and v3.20.1 on 2026-05-26. That is a steady cadence, with the gap between v3.20.1 and v3.20.2 spanning roughly three months and the gap to v3.21.0 much shorter.

Upgrade cost is dominated by the dependency set rather than by Testem's own API. The package depends on express ^5.2.1, chokidar ^5.0.0, commander ^14.0.3, glob ^13.0.6, minimatch ^10.2.5 and socket.io ^4.8.3, among others. Those are all major-version-pinned ranges, so a major bump in any of them lands in your tree the next time you reinstall unless your lockfile holds it. The engines field is the other upgrade gate: the current range starts at ^20.19.0, so a project still on an older Node line cannot install this version at all. Plan Node upgrades before Testem upgrades, not after.

The licence is MIT, declared in package.json and present as LICENSE.md at the repository root. MIT is permissive and imposes essentially no conditions beyond retaining the copyright and permission notice in distributions. That is a statement about what the licence text says, not legal advice; if your organisation has a policy on third-party licences, route it through whoever owns that policy. The transitive dependency licences are not enumerated in the repository files, so the licence of the bundle as a whole is not something this review can state.

Editorial conclusion

Adopt Testem if your suite needs to execute inside actual browser engines, or if you want one runner across Jasmine, QUnit, Mocha and custom adapters without rewriting specs. Skip it if you only need headless Node unit tests, since Node's own runner covers that with no browser launcher to maintain. Before committing, verify your Node version satisfies the engines range in package.json, confirm your CI can launch the browser you intend to use, and check that your watch patterns behave under your filesystem, since the README points to CHOKIDAR_USE_POLLING for Docker and network mounts.

Frequently asked questions

What is Testem and what does it run?

Testem is a JavaScript test runner that executes your tests in real desktop browsers such as Chrome, Firefox, Safari and Edge, and can also target Node or headless Chrome via browser_args. It is framework-agnostic, with adapters for Jasmine, QUnit and Mocha and support for custom adapters.

How do I install Testem?

The README recommends installing it both as a dev dependency so your project pins a version, and globally so the testem command is on your PATH, using npm install testem --save-dev and npm install testem -g. It requires a supported Node.js runtime, currently ^20.19.0, ^22.12.0, ^24.0.0 or >= 26.0.0.

How do I run Testem in continuous integration?

The README gives testem ci as the CI command, and notes that GitHub Actions is a common way to run it, often with the Headless Chrome or Chromium launcher. The project's own workflow lives in .github/workflows/ci.yml.

Why does Testem not detect my file changes in Docker?

The README states that on some setups such as Docker, network filesystems and VMs, native fs.watch can be flaky. Chokidar supports CHOKIDAR_USE_POLLING=1 to force polling and CHOKIDAR_INTERVAL to set the polling interval in milliseconds.

How do I turn off Testem's file watcher?

The config key disable_watching set to true turns the file watcher off entirely, according to the README's file watching section. The related keys are src_files, watch_files and src_files_ignore.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. testem/testem on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/testem-testem.svg)](https://hysenlabs.com/projects/testem-testem)