npm install --save-dev @stryker-mutator/core
We're excited to unveil the next major iteration of the Stryker mutation testing framework for JavaScript and its ecosystem: version 4.0. This release represents a significant internal overhaul, moving to a technique called "mutation switching" that boosts both performance and developer experience — aligning Stryker with how its sibling projects Stryker.NET (C#) and Stryker4s (Scala) already operate. Naturally, such a fundamental shift introduces some breaking changes; details are outlined in the breaking changes section.
For those unfamiliar, mutation testing gauges how well your test suite detects faults. The framework systematically introduces small, deliberate changes — known as *mutants* — into your source code. It then executes your tests: if a test fails, the mutant is "killed"; if all tests still pass, the mutant has "survived", signaling a potential gap in your test coverage. The mutation testing report gives you a birds-eye view of which test cases you might have overlooked. Should this sound a bit abstract, we recommend checking out our RoboBar example for a concrete demonstration.
Getting Started
New to Stryker? The quickstart guide is your best first step, along with the Stryker homepage for an overview. Existing users can upgrade using their preferred package manager. Remember to update your Stryker plugins to the latest versions as well. The readme for@stryker-mutator/core contains a full list of configurable options.
Heads up: these plugins have been deprecated and removed, so you can safely delete them from your package.json:
- @stryker-mutator/typescript
- @stryker-mutator/jasmine-framework
- @stryker-mutator/mocha-framework
- @stryker-mutator/babel-transpiler
- @stryker-mutator/webpack-transpiler
- @stryker-mutator/javascript-mutator
Mutation Switching
The core principle of mutation switching is embedding *all* mutants into your codebase at the same time, but activating only *one* mutant during any given test run. To illustrate, consider this simple function:function add(a, b) {
return a + b;
}
Feeding this snippet to Stryker yields two distinct mutants:
- function add(a, b) { return a - b }
- function add(a, b) {}
In earlier versions of Stryker, this would have meant creating duplicate copies of your entire codebase — one per mutant — and running the full test suite against each copy separately. Stryker 4.0, however, interleaves all mutants into a single copy of the code, activating each one for the duration of the test run, then seamlessly moving on to the next. The internal representation looks something like this (simplified):
function add(a, b) {
if (global.activeMutant === 0) {
// ?
} else {
return global.activeMutant === 1
? a - b // ?
: a + b;
}
}
This approach—running your tests and simply toggling between mutants—can lead to significant performance gains, particularly on larger or more complex projects. The exact improvement is highly dependent on your setup. For instance, if you typically bundle your code with webpack before testing, webpack now only needs to execute once for *all* mutants, resulting in a massive speedup. But if you're running an older ES5 codebase with mocha without any bundling, the gains might be more modest. We've consistently observed a general speed increase of 20% to 70%.
Code Instrumentation
The efficiency benefits of mutation switching don't come for free — they impose significant new demands on the transformer Stryker applies to your code. Precisely, this means navigating the Abstract Syntax Tree (AST) to collect and position every mutant. To accomplish this, Stryker 4.0 has integrated the Babel parser directly into@stryker-mutator/core. This strategic choice taps into Babel's comprehensive support for the entire JavaScript ecosystem, including TypeScript.
It's worth noting that, without Babel v7's built-in TypeScript support, building mutation switching for both TypeScript and JavaScript would have essentially meant developing the feature twice. We're genuinely impressed by the Babel team’s decision to incorporate TypeScript, and we stand proudly on the shoulders of these giants.
This shift in instrumentation means configuring a separate mutator is now a thing of the past. Stryker automatically applies your Babel configuration and can handle TypeScript, JavaScript, Flow, and JSX. It even parses and mutates code hidden inside <script> tags in your .html or .vue files. No setup required—truly a gift for the whole community.
Build Command Support
First, a caveat: not every project strictly requires a dedicated build step. Some can deploy plain JavaScript or execute source TypeScript directly via tools like ts-node, babel/register, or by using a runner like Jest or Karma with a webpack plugin. The previous version of Stryker offered a "transpiler" configuration option. For example, setting"transpilers": ["babel"] relied on the @stryker-mutator/babel-transpiler plugin to transpile all code. This older approach had three notable drawbacks:
- **Performance bottleneck:** Transpiling each mutant individually was a notoriously slow process.
- **Plugin ecosystems:** Every transpiler or bundler required its own dedicated Stryker plugin. Officially, only Webpack, Babel, and TypeScript had this support, leaving users of other tools out in the cold.
- **Maintenance overhead:** These transpiler plugins were invasive, hooking deep into the APIs of respective tools, making them difficult to maintain effectively.
Stryker 4.0 completely abandons the concept of transpiler plugins in favor of a configurable buildCommand. This command is executed exactly once in your sandbox directory — after instrumentation, but before the initial dry run. For example, specifying --buildCommand "npm run build" tells Stryker to run that script inside the sandbox. This simple change elegantly resolves all three previous disadvantages: it runs only once, it's tool-agnostic, and it removes a significant maintenance burden from our plate. The corollary is that the old transpiler plugins are no longer required and should be stripped from your project's dependencies.
Checker Plugin
You might recall that the old@stryker-mutator/typescript transpiler had a dual role: it not only handled transpilation but also filtered out mutants that introduced type errors, classifying them with the status "compile error" in your report. These invalid mutants were excluded from your overall mutation score.
With the removal of the transpiler plugin in favor of --buildCommand, a question arises: how does your TypeScript code manage to compile when some mutants introduce type errors?
The answer lies in a small header comment. Stryker prepends // @no-check to your source files, which instructs the TypeScript compiler to ignore type errors that are solely artifacts of the mutation instrumentation.
Nonetheless, you might still want a way to systematically weed out mutants that would cause type errors, keeping them from cluttering your report. This is precisely where the new "Checker" plugin shines. These plugins hold the logic to determine a mutant's validity based on distinct criteria.
We provide one such checker: the @stryker-mutator/typescript-checker. This plugin will identify and mark any mutant that produces a type error with the "Compile error" status.
You can install it via `npm install -D @stryker-mutator/typescript-checker`. Then, configure it using the following snippet:
{
"checkers": ["typescript"],
"tsconfigFile": "tsconfig.json"
}
For more in-depth documentation, please refer to the TypeScript checker's readme.
Coverage Analysis
Coverage analysis is a potent tool for accelerating mutation testing by reducing the number of tests executed per mutant. Stryker offers three levels of coverage analysis: - **"off"** — No coverage analysis is performed. - **"all"** — Measures which parts of the test suite cover each mutant. Mutants that aren't touched by any test are flagged as "No coverage"; however, for a covered mutant, the *entire* test suite is run. - **"perTest"** — Tracks coverage down to the individual test level. Stryker runs only the minimal set of tests required to cover a specific mutant when it's testing that mutant. Historically, adopting the "perTest" mode yielded phenomenal performance gains (typically a 40% to 60% reduction in time). Regrettably, very few projects could leverage this potential. That's because "perTest" analysis relied on integrating istanbul code coverage with specific test runner hooks—a combination that worked exclusively in environments without any transpiling or bundling steps. Version 4.0 flips this reality on its head. Now, the same instrumentation process that injects mutants also handles coverage tracking. This removes the external dependency on istanbul and—critically—makes precise mutant coverage possible *regardless of* your transpiler or bundler setup. The only prerequisite that remains is the test runner's support for hooks. Fortunately, every Stryker test runner plugin supports these hooks, with one exception: the@stryker-mutator/jest-runner plugin doesn't support it just yet, though it's on our roadmap for future implementation.
Other Changes
We've also used this major release to continue cleaning up the internal architecture of Stryker, pruning away old extension points to reduce complexity. **Removal ofTestFramework plugins:** Historically, you'd need to configure a TestFramework plugin separate from your runner. Its job was to allow Stryker to hook into a testing framework (like mocha or jasmine) to enable "perTest" coverage. In practice, the framework's logic and the test runner's logic were inseparable—mocha's runner always uses the mocha framework. This bundling made configuring Stryker far more convoluted than it needed to be. To streamline this, we've eliminated the TestFramework plugin type entirely and moved that responsibility directly into the Test Runner plugins. Consequently, you can now uninstall @stryker-mutator/mocha-framework and @stryker-mutator/jasmine-framework.
**Removal of OptionsEditor plugins:** Another obsolete plugin type, OptionsEditor, provided plugins a way to directly read and modify the global Stryker options object, often to add their own configuration settings. This indirect mechanism has been replaced with a simpler directive: plugins are now expected to load their required options during their own lifecycle (for example, in the plugin's init method). Goodbye, OptionsEditor.
Notable breaking changes
The most impactful breaking changes are outlined below. For a complete list, refer to the changelog.
- The
transpilersconfiguration option has been removed; switch to using--buildCommand. - Specifying
mutatoras a string or settingmutator.nameis no longer valid. Stryker now always relies on its built-in code instrumenter. - TypeScript 3.7+ is mandatory when using TypeScript for transpilation, whether through a
--buildCommandor via a test runner plugin such as ts-jest, karma-webpack, or the angular-cli. If you are stuck on TS < 3.7, either upgrade or stick with Stryker V3. This requirement stems from the mutation instrumenter's heavy reliance on the// @ts-nocheckdirective introduced in TS3.7. Other transpilers like babel or ts-node withtranspileOnlywork fine without this constraint. - Angular projects now require Angular >=9.0 due to the TS 3.7 prerequisite.
- The
"command"test runner operates on a "best-effort" basis. An environment variable is used to signal which mutant is active, and your test command must propagate this variable to the test environment. This approach should work well for most runners and commands. Karma is a notable exception, but the@stryker-mutator/karma-runnercovers that scenario. - Web Component Tester support has been discontinued. The rationale is documented in #2386.
- Exporting a
functionfromstryker.conf.jsis now deprecated. Export a plain object or use astryker.conf.jsonfile instead. Further details can be found in #2429. - The
--maxConcurrentTestRunnerflag is deprecated in favor of--concurrency. This setting now governs how Stryker scales checkers and test runner processes. Be aware that Stryker will no longer cap this value at your machine's logical core count, so setting--concurrency 9999is likely unwise.
What lies ahead
Implementing mutation switching was a hefty endeavor, but it paves the way for significant gains in performance and usability. Here's a look at what's on the horizon.
These priorities are aligned with the goals outlined in our roadmap.
Enhanced Jest integration
With mutation switching in place, adding "perTest" coverage analysis for Jest is now feasible. This would enable Stryker to execute a smaller subset of tests overall. Track the progress in #2316.
Hot reload capability
Currently, Stryker refreshes all code files between test runs, either by purging the require.cache or triggering a full page reload in Karma. Mutation switching changes this: the mutated code can stay loaded, and we can simply toggle the active mutant before running tests again. Mocha will be the first to benefit from this optimization. See #2413 for details.
In-place mutation
Today, Stryker never alters your source code directly. Instead, it duplicates your project into a "sandbox" folder and mutates that copy. The reasoning is clear: you don't want mutants accidentally ending up in production.
But there are edge cases where merely copying the code into a sandbox breaks the test setup entirely. Examples are listed in #2163. To make Stryker genuinely compatible with every JavaScript project, we'll need to offer "in place" mutation. This will be an opt-in feature, and we'll clearly communicate what Stryker is doing under the hood.
Acknowledgments
When we shipped the first beta of Stryker 4 back in July, we anticipated feedback—but the response exceeded our expectations. Your input helped us close over 40 issues, several of which were substantial. We're deeply grateful to everyone who contributed to making this release possible. Special thanks go out to (in no particular order)
gramster
kmdrGroch
Lakitna
brodybits
Garethp
You all are amazing!
And if you've made it this far: thank you for reading! Now go give Stryker 4 a spin and tell us how it goes.
