Skip to content

Commit

Permalink
feat(Jest): support overriding config (#2197)
Browse files Browse the repository at this point in the history
* Adds support for overriding config in your package.json/jest.config.js and/or react-scripts node_modules.
* Added config setting `jest.configFile` to specify a path to your config file. This file will be loaded using `require`
* The `jest.config` setting now works together with `jest.configFile` to allow you to override the config in your `configFile`

Fixes #2155
  • Loading branch information
nicojs committed May 13, 2020
1 parent 2425b1a commit d37b7d7
Show file tree
Hide file tree
Showing 22 changed files with 284 additions and 459 deletions.
7 changes: 5 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@
"request": "attach",
"name": "Attach to Port",
"address": "localhost",
"port": 9229
"port": 9229,
"skipFiles": [
"<node_internals>/**"
]
}
]
}
}
18 changes: 10 additions & 8 deletions packages/jest-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,26 @@ The @stryker-mutator/jest-runner is a plugin for Stryker to enable Jest as a tes
For the minimum supported versions, see the peerDependencies section in package.json.

## Configuration

### Configuring Stryker
Make sure you set the `testRunner` option to "jest" and set `coverageAnalysis` to "off" in your Stryker configuration.

```javascript
{
testRunner: 'jest'
testRunner: 'jest',
coverageAnalysis: 'off'
}
```

### Configuring Jest
### Advanced configuration
The @stryker-mutator/jest-runner also provides a couple of configurable options using the `jest` property in your Stryker config:

```javascript
{
jest: {
projectType: 'custom',
config: require('path/to/your/custom/jestConfig.js'),
configFile: 'path/to/your/custom/jestConfig.js',
config: {
testEnvironment: 'jest-environment-jsdom-sixteen'
},
enableFindRelatedTests: true,
}
}
Expand All @@ -51,11 +52,12 @@ The @stryker-mutator/jest-runner also provides a couple of configurable options
| projectType (optional) | The type of project you are working on. | `custom` | `custom` uses the `config` option (see below)|
| | | | `create-react-app` when you are using [create-react-app](https://github.com/facebook/create-react-app) |
| | | | `create-react-app-ts` when you are using [create-react-app-typescript](https://github.com/wmonk/create-react-app-typescript) |
| config (optional) | A custom Jest configuration object. You could also use `require` to load it here. | undefined | |
| configFile (optional) | The path to your Jest config file. | undefined | |
| config (optional) | Custom Jest config. This will override file-based config. | undefined | |
| enableFindRelatedTests (optional) | Whether to run jest with the `--findRelatedTests` flag. When `true`, Jest will only run tests related to the mutated file per test. (See [_--findRelatedTests_](https://jestjs.io/docs/en/cli.html#findrelatedtests-spaceseparatedlistofsourcefiles)) | true | false |

**Note:** When neither of the options are specified it will use the Jest configuration in your "package.json". \
**Note:** the `projectType` option is ignored when the `config` option is specified.
**Note:** When the projectType is `custom` and no `configFile` is specified, your `jest.config.js` or `package.json` will be loaded. \
**Note:** The `configFile` setting is **not** supported for `create-react-app` and `create-react-app-ts`. \
**Note:** Stryker currently only works for CRA-projects that have not been [_ejected_](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-eject).

The following is an example stryker.conf.js file:
Expand Down
6 changes: 5 additions & 1 deletion packages/jest-runner/schema/jest-runner-options.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
"$ref": "#/definitions/jestProjectType",
"default": "custom"
},
"configFile": {
"description": "Path to your Jest config file. Please leave it empty if you want jest configuration to be loaded from package.json or a standard jest configuration file.",
"type": "string"
},
"config": {
"description": "A custom Jest configuration object. You could also use `require` to load it here. Please leave it empty if you want jest configuration to be loaded from package.json or a standard jest configuration file.",
"description": "A custom Jest configuration object. You could also use `require` to load it here.",
"type": "object"
},
"enableFindRelatedTests": {
Expand Down
53 changes: 0 additions & 53 deletions packages/jest-runner/src/JestOptionsEditor.ts

This file was deleted.

34 changes: 24 additions & 10 deletions packages/jest-runner/src/JestTestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,41 @@ import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, Injector, OptionsContext, tokens } from '@stryker-mutator/api/plugin';
import { RunOptions, RunResult, RunStatus, TestResult, TestRunner, TestStatus } from '@stryker-mutator/api/test_runner';

import { JEST_VERSION_TOKEN, jestTestAdapterFactory } from './jestTestAdapters';
import { jestTestAdapterFactory } from './jestTestAdapters';
import JestTestAdapter from './jestTestAdapters/JestTestAdapter';
import { JestRunnerOptionsWithStrykerOptions } from './JestRunnerOptionsWithStrykerOptions';
import JestConfigLoader from './configLoaders/JestConfigLoader';
import { configLoaderToken, processEnvToken, jestTestAdapterToken, jestVersionToken } from './pluginTokens';
import { configLoaderFactory } from './configLoaders';
import JEST_OVERRIDE_OPTIONS from './jestOverrideOptions';

export function jestTestRunnerFactory(injector: Injector<OptionsContext>) {
return injector
.provideValue(PROCESS_ENV_TOKEN, process.env)
.provideValue(JEST_VERSION_TOKEN, require('jest/package.json').version as string)
.provideFactory(JEST_TEST_ADAPTER_TOKEN, jestTestAdapterFactory)
.provideValue(processEnvToken, process.env)
.provideValue(jestVersionToken, require('jest/package.json').version as string)
.provideFactory(jestTestAdapterToken, jestTestAdapterFactory)
.provideFactory(configLoaderToken, configLoaderFactory)
.injectClass(JestTestRunner);
}
jestTestRunnerFactory.inject = tokens(commonTokens.injector);

export const PROCESS_ENV_TOKEN = 'PROCESS_ENV_TOKEN';
export const JEST_TEST_ADAPTER_TOKEN = 'jestTestAdapter';

export default class JestTestRunner implements TestRunner {
private readonly jestConfig: Jest.Configuration;

private readonly enableFindRelatedTests: boolean;

public static inject = tokens(commonTokens.logger, commonTokens.options, PROCESS_ENV_TOKEN, JEST_TEST_ADAPTER_TOKEN);
public static inject = tokens(commonTokens.logger, commonTokens.options, processEnvToken, jestTestAdapterToken, configLoaderToken);
constructor(
private readonly log: Logger,
options: StrykerOptions,
private readonly processEnvRef: NodeJS.ProcessEnv,
private readonly jestTestAdapter: JestTestAdapter
private readonly jestTestAdapter: JestTestAdapter,
configLoader: JestConfigLoader
) {
const jestOptions = options as JestRunnerOptionsWithStrykerOptions;
// Get jest configuration from stryker options and assign it to jestConfig
this.jestConfig = (jestOptions.jest.config as unknown) as Jest.Configuration;
const configFromFile = configLoader.loadConfig();
this.jestConfig = this.mergeConfigSettings(configFromFile, (jestOptions.jest.config as any) || {});

// Get enableFindRelatedTests from stryker jest options or default to true
this.enableFindRelatedTests = jestOptions.jest.enableFindRelatedTests;
Expand Down Expand Up @@ -111,4 +115,14 @@ export default class JestTestRunner implements TestRunner {
return TestStatus.Skipped;
}
}

private mergeConfigSettings(configFromFile: Jest.Configuration, config: Jest.Configuration) {
const stringify = (obj: object) => JSON.stringify(obj, null, 2);
this.log.trace(
`Merging file-based config ${stringify(configFromFile)}
with custom config ${stringify(config)}
and default (internal) stryker config ${JEST_OVERRIDE_OPTIONS}`
);
return Object.assign(configFromFile, config, JEST_OVERRIDE_OPTIONS);
}
}
29 changes: 23 additions & 6 deletions packages/jest-runner/src/configLoaders/CustomJestConfigLoader.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import fs = require('fs');
import path from 'path';

import { Logger } from '@stryker-mutator/api/logging';
import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { StrykerOptions } from '@stryker-mutator/api/core';

import { loaderToken, projectRootToken } from '../pluginTokens';
import { JestRunnerOptionsWithStrykerOptions } from '../JestRunnerOptionsWithStrykerOptions';

import JestConfigLoader from './JestConfigLoader';
import { NodeRequireFunction } from './NodeRequireFunction';

/**
* The Default config loader will load the Jest configuration using the package.json in the package root
*/
export default class CustomJestConfigLoader implements JestConfigLoader {
private readonly _projectRoot: string;
public static inject = tokens(commonTokens.logger, commonTokens.options, loaderToken, projectRootToken);

constructor(projectRoot: string, private readonly _loader: NodeRequireFunction = require) {
this._projectRoot = projectRoot;
}
constructor(
private readonly log: Logger,
private readonly options: StrykerOptions,
private readonly require: NodeRequireFunction,
private readonly projectRoot: string
) {}

public loadConfig(): Jest.Configuration {
const jestConfig = this.readConfigFromJestConfigFile() || this.readConfigFromPackageJson() || {};
Expand All @@ -21,15 +31,22 @@ export default class CustomJestConfigLoader implements JestConfigLoader {

private readConfigFromJestConfigFile() {
try {
return this._loader(path.join(this._projectRoot, 'jest.config.js'));
const jestOptions = this.options as JestRunnerOptionsWithStrykerOptions;
const configFilePath = path.join(this.projectRoot, jestOptions.jest?.configFile || 'jest.config.js');
const config = this.require(configFilePath);
this.log.debug(`Read Jest config from ${configFilePath}`);
return config;
} catch {
/* Don't return anything (implicitly return undefined) */
}
}

private readConfigFromPackageJson() {
try {
return JSON.parse(fs.readFileSync(path.join(this._projectRoot, 'package.json'), 'utf8')).jest;
const configFilePath = path.join(this.projectRoot, 'package.json');
const config = JSON.parse(fs.readFileSync(configFilePath, 'utf8')).jest;
this.log.debug(`Read Jest config from ${configFilePath}`);
return config;
} catch {
/* Don't return anything (implicitly return undefined) */
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import path from 'path';

import { tokens } from '@stryker-mutator/api/plugin';

import { createReactJestConfig } from '../utils/createReactJestConfig';
import { projectRootToken, resolveToken } from '../pluginTokens';

import JestConfigLoader from './JestConfigLoader';

export default class ReactScriptsJestConfigLoader implements JestConfigLoader {
private readonly projectRoot: string;
public static inject = tokens(resolveToken, projectRootToken);

constructor(projectRoot: string, private readonly resolve: RequireResolve = require.resolve) {
this.projectRoot = projectRoot;
}
constructor(private readonly resolve: RequireResolve, private readonly projectRoot: string) {}

public loadConfig(): Jest.Configuration {
try {
Expand All @@ -19,9 +20,6 @@ export default class ReactScriptsJestConfigLoader implements JestConfigLoader {
// Create the React configuration for Jest
const jestConfiguration = this.createJestConfig(reactScriptsLocation);

// Set test environment to jsdom (otherwise Jest won't run)
jestConfiguration.testEnvironment = 'jsdom';

return jestConfiguration;
} catch (e) {
if (this.isNodeErrnoException(e) && e.code === 'MODULE_NOT_FOUND') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as path from 'path';

import { tokens } from '@stryker-mutator/api/plugin';

import { createReactTsJestConfig } from '../utils/createReactJestConfig';
import { projectRootToken, resolveToken } from '../pluginTokens';

import JestConfigLoader from './JestConfigLoader';

export default class ReactScriptsTSJestConfigLoader implements JestConfigLoader {
private readonly projectRoot: string;
public static inject = tokens(resolveToken, projectRootToken);

constructor(projectRoot: string, private readonly resolve = require.resolve) {
this.projectRoot = projectRoot;
}
constructor(private readonly resolve: RequireResolve, private readonly projectRoot: string) {}

public loadConfig(): Jest.Configuration {
try {
Expand All @@ -18,10 +19,7 @@ export default class ReactScriptsTSJestConfigLoader implements JestConfigLoader

// Create the React configuration for Jest
const jestConfiguration = this.createJestConfig(reactScriptsTsLocation);

// Set test environment to jsdom (otherwise Jest won't run)
jestConfiguration.testEnvironment = 'jsdom';

return jestConfiguration;
} catch (e) {
if (this.isNodeErrnoException(e) && e.code === 'MODULE_NOT_FOUND') {
Expand Down
50 changes: 50 additions & 0 deletions packages/jest-runner/src/configLoaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { tokens, commonTokens, Injector, OptionsContext } from '@stryker-mutator/api/plugin';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';

import { JestRunnerOptionsWithStrykerOptions } from '../JestRunnerOptionsWithStrykerOptions';
import { loaderToken, resolveToken, projectRootToken } from '../pluginTokens';

import CustomJestConfigLoader from './CustomJestConfigLoader';
import ReactScriptsJestConfigLoader from './ReactScriptsJestConfigLoader';
import ReactScriptsTSJestConfigLoader from './ReactScriptsTSJestConfigLoader';

configLoaderFactory.inject = tokens(commonTokens.options, commonTokens.injector, commonTokens.logger);
export function configLoaderFactory(options: StrykerOptions, injector: Injector<OptionsContext>, log: Logger) {
const warnAboutConfigFile = (projectType: string, configFile: string | undefined) => {
if (configFile) {
log.warn(`Config setting "configFile" is not supported for projectType "${projectType}"`);
}
};
const optionsWithJest: JestRunnerOptionsWithStrykerOptions = options as JestRunnerOptionsWithStrykerOptions;

const configLoaderInjector = injector
.provideValue(loaderToken, require)
.provideValue(resolveToken, require.resolve)
.provideValue(projectRootToken, process.cwd());

switch (optionsWithJest.jest.projectType) {
case 'custom':
return configLoaderInjector.injectClass(CustomJestConfigLoader);
case 'create-react-app':
warnAboutConfigFile(optionsWithJest.jest.projectType, optionsWithJest.jest.configFile);
return configLoaderInjector.injectClass(ReactScriptsJestConfigLoader);
case 'create-react-app-ts':
warnAboutConfigFile(optionsWithJest.jest.projectType, optionsWithJest.jest.configFile);
return configLoaderInjector.injectClass(ReactScriptsTSJestConfigLoader);
case 'react':
log.warn(
'DEPRECATED: The projectType "react" is deprecated. Use projectType "create-react-app" for react projects created by "create-react-app" or use "custom" for other react projects.'
);
warnAboutConfigFile(optionsWithJest.jest.projectType, optionsWithJest.jest.configFile);
return configLoaderInjector.injectClass(ReactScriptsJestConfigLoader);
case 'react-ts':
log.warn(
'DEPRECATED: The projectType "react-ts" is deprecated. Use projectType "create-react-app-ts" for react projects created by "create-react-app" or use "custom" for other react projects.'
);
warnAboutConfigFile(optionsWithJest.jest.projectType, optionsWithJest.jest.configFile);
return configLoaderInjector.injectClass(ReactScriptsTSJestConfigLoader);
default:
throw new Error(`No configLoader available for ${optionsWithJest.jest.projectType}`);
}
}
13 changes: 6 additions & 7 deletions packages/jest-runner/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { declareClassPlugin, declareFactoryPlugin, PluginKind } from '@stryker-mutator/api/plugin';
import { declareFactoryPlugin, PluginKind } from '@stryker-mutator/api/plugin';

import strykerValidationSchema from '../schema/jest-runner-options.json';

import JestOptionsEditor from './JestOptionsEditor';
import { jestTestRunnerFactory } from './JestTestRunner';

process.env.BABEL_ENV = 'test';

export const strykerPlugins = [
declareClassPlugin(PluginKind.OptionsEditor, 'jest', JestOptionsEditor),
declareFactoryPlugin(PluginKind.TestRunner, 'jest', jestTestRunnerFactory),
];
export * as strykerValidationSchema from '../schema/jest-runner-options.json';
export const strykerPlugins = [declareFactoryPlugin(PluginKind.TestRunner, 'jest', jestTestRunnerFactory)];

export { strykerValidationSchema };
Loading

0 comments on commit d37b7d7

Please sign in to comment.