Skip to main content

Create a plugin

You can extend Stryker with the following plugin kinds:

export enum PluginKind {
Checker = 'Checker',
TestRunner = 'TestRunner',
Reporter = 'Reporter',
}

They are loaded using the plugins configuration option

Each plugin has it's own job to do. For inspiration, check out the stryker monorepo.

Creating a plugin#

Creating plugins is best done with typescript, as that will help you a lot with type safety and intellisense. We provide the @stryker-mutator/api dependency on the types and basic helper functionality. This should be installed as a dependency of your plugin.

npm install @stryker-mutator/api

Next, you need to create a class that is the actual plugin. For example:

import { TestRunner, DryRunResult, DryRunOptions, MutantRunOptions, MutantRunResult } from '@stryker-mutator/api/test-runner';
class FooTestRunner implements TestRunner {
public init(): Promise<void> {
// TODO: Implement or remove
}
public dryRun(options: DryRunOptions): Promise<DryRunResult> {
// TODO: Implement
}
public mutantRun(options: MutantRunOptions): Promise<MutantRunResult> {
// TODO: Implement
}
public dispose(): Promise<void> {
// TODO: Implement or remove
}
}

In this example, a TestRunner plugin is constructed. Each plugin kind has it's own interface, so it is easy to get started with a skeleton implementation.

After you've created your skeleton plugin, you're ready to declare it.

Declaring your plugin#

In order to make your plugin known to Stryker, you should export the declaration of your plugin. You can either declare it as a factory method or a class. Stryker will take care of creating your plugin implementation at the correct moment in the Stryker lifecycle.

A class example:

// index.ts
import FooTestRunner from './foo-test-runner';
import { PluginKind, declareClassPlugin } from '@stryker-mutator/api/plugin';
export const strykerPlugins = [declareClassPlugin(PluginKind.TestRunner, 'foo', FooTestRunner)];

A factory method example (useful when you want to inject additional values/classes into the DI system):

// index.ts
import FooTestRunner from './foo-test-runner';
import FooTestRunnerConfigFileLoader from './foo-test-runner-config-file-loader';
import { configLoaderToken, processEnvToken, fooTestRunnerVersionToken } from './plugin-tokens';
import { declareFactoryPlugin, PluginKind } from '@stryker-mutator/api/plugin';
const createFooTestRunner = createFooTestRunnerFactory();
export function createFooTestRunnerFactory() {
createFooTestRunner.inject = tokens(commonTokens.injector);
function createFooTestRunner(injector: Injector<PluginContext>): FooTestRunner {
return injector
.provideValue(processEnvToken, process.env)
.provideValue(fooTestRunnerVersionToken, require('fooTestRunner/package.json').version as string)
.provideClass(configLoaderToken, FooTestRunnerConfigFileLoader)
.injectClass(FooTestRunner);
}
return createFooTestRunner;
}
export const strykerPlugins = [declareFactoryPlugin(PluginKind.TestRunner, 'foo', createFooTestRunner)];

Now you're ready to test out your plugin!

Test your plugin#

It is easy to test your plugin on a test project by loading it via the plugins section.

For example, when your test project resides next to your plugin implementation:

// stryker.conf.js
module.exports = {
testRunner: 'foo', // name of your test runner
plugins: ['@stryker-mutator/*', require.resolve('../my-plugin')], // load your test runner here
concurrency: 1, // useful for debugging your
testRunnerNodeArgs: ['--inspect'] // useful for debugging your test runner plugin
};

Note: Be sure you have compiled your TypeScript correctly.

You can test it out with Stryker:

npx stryker run

Test runner and checker plugins are actually created in its own child process. Therefore you cannot debug them directly. Instead you can use the testRunnerNodeArgs: ['--inspect'] to debug your test runner plugin (an equivalent for the checker plugin isn't created yet, please let us know if you need it).

After you've verified that your plugin loads correctly, it is recommended to create your own integration tests and not rely on Stryker to test it out each time. This will allow you to develop your plugin faster.

Dependency injection#

Stryker uses typed-inject as a dependency injection (DI) framework. It is recommended that you also use this as your DI framework inside the plugin.

See this example below.

import { StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, PluginContext } from '@stryker-mutator/api/plugin';
import { TestRunner, DryRunResult, DryRunOptions, MutantRunOptions, MutantRunResult } from '@stryker-mutator/api/test-runner';
import * as pluginTokens from './plugin-tokens';
import FooTestRunnerConfigFileLoader from './foo-test-runner-config-file-loader';
export class FooTestRunner implements TestRunner {
public static inject = [
commonTokens.logger,
commonTokens.options,
pluginTokens.configLoader,
pluginTokens.processEnv,
pluginTokens.fooTestRunnerVersion
] as const;
constructor(
private readonly log: Logger,
private readonly options: StrykerOptions,
private readonly configLoader: FooTestRunnerConfigFileLoader,
private readonly processEnvRef: NodeJS.ProcessEnv,
private readonly fooTestRunnerVersion: string
) { }
public init(): Promise<void> {
// TODO: Implement or remove
}
public dryRun(options: DryRunOptions): Promise<DryRunResult> {
// TODO: Implement
}
public mutantRun(options: MutantRunOptions): Promise<MutantRunResult> {
// TODO: Implement
}
public dispose(): Promise<void> {
// TODO: Implement or remove
}
}
export function fooTestRunnerFactory(injector: Injector<PluginContext>) {
return injector
.provideValue(pluginTokens.processEnv, process.env)
.provideValue(pluginTokens.fooTestRunnerVersion, require('foo/package.json').version as string)
.provideClass(pluginTokens.configLoader, FooTestRunnerConfigFileLoader)
.injectClass(FooTestRunner);
}
fooTestRunnerFactory.inject = [commonTokens.injector] as const;

In this example, you can see that some tokens are loaded from commonTokens and some are loaded from pluginTokens.

  • commonTokens: These contain the tokens belonging to values Stryker itself provide.
  • pluginTokens: These are an example of tokens you can provide yourself in your plugin. The fooTestRunnerFactory factory method is an example of where the tokens are provided.

This is type-safe. When you declare your plugin, TypedInject will validate that you don't inject something that cannot be resolved at runtime.

What's next?#

If you have a plugin that you think other users might be able to benefit from, or you simply need some help, please let us know on Slack.

We're always looking to promote user-created plugins ๐Ÿ’—