Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Jest): support overriding config #2197

Merged
merged 15 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simondel this made the test succeed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it turns out react-scripts does use 'jsdom' as a default (facebook/create-react-app#5074) but react-scripts-ts was forked before that change was made and it hasn't really been updated for two years...


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