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

fix(JestTestRunner): run jest with --findRelatedTests #1235

Merged
merged 10 commits into from
Nov 29, 2018
2 changes: 2 additions & 0 deletions packages/stryker-api/src/test_runner/RunOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface RunOptions {
* It should be loaded right after the test framework but right before any tests can run.
*/
testHooks?: string;

mutatedFileName?: string;
}

export default RunOptions;
4 changes: 3 additions & 1 deletion packages/stryker-jest-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ The stryker-jest-runner also provides a couple of configurable options using the
{
jest: {
projectType: 'custom',
config: require('path/to/your/custom/jestConfig.js')
config: require('path/to/your/custom/jestConfig.js'),
enableFindRelatedTests: true,
}
}
```
Expand All @@ -51,6 +52,7 @@ The stryker-jest-runner also provides a couple of configurable options using the
| | | | `react` when you are using [create-react-app](https://github.com/facebook/create-react-app) |
| | | | `react-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 | |
| 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.
Expand Down
19 changes: 16 additions & 3 deletions packages/stryker-jest-runner/src/JestTestRunner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getLogger } from 'stryker-api/logging';
import { RunnerOptions, RunResult, TestRunner, RunStatus, TestResult, TestStatus } from 'stryker-api/test_runner';
import { RunnerOptions, RunResult, TestRunner, RunStatus, TestResult, TestStatus, RunOptions } from 'stryker-api/test_runner';
import * as jest from 'jest';
import JestTestAdapterFactory from './jestTestAdapters/JestTestAdapterFactory';

export default class JestTestRunner implements TestRunner {
private readonly log = getLogger(JestTestRunner.name);
private readonly jestConfig: jest.Configuration;
private readonly processEnvRef: NodeJS.ProcessEnv;
private readonly enableFindRelatedTests: boolean;

public constructor(options: RunnerOptions, processEnvRef?: NodeJS.ProcessEnv) {
// Make sure process can be mocked by tests by passing it in the constructor
Expand All @@ -15,19 +16,31 @@ export default class JestTestRunner implements TestRunner {
// Get jest configuration from stryker options and assign it to jestConfig
this.jestConfig = options.strykerOptions.jest.config;

// Get enableFindRelatedTests from stryker jest options or default to true
this.enableFindRelatedTests = options.strykerOptions.jest.enableFindRelatedTests;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can log on debug what we are doing with this setting?

if (this.enableFindRelatedTests === undefined) {
this.enableFindRelatedTests = true;
}

if (this.enableFindRelatedTests) {
this.log.debug('Running jest with --findRelatedTests flag. Set jest.enableFindRelatedTests to false to run all tests on every mutant.');
} else {
this.log.debug('Running jest without --findRelatedTests flag. Set jest.enableFindRelatedTests to true to run only relevant tests on every mutant.');
}

// basePath will be used in future releases of Stryker as a way to define the project root
// Default to process.cwd when basePath is not set for now, should be removed when issue is solved
// https://github.com/stryker-mutator/stryker/issues/650
this.jestConfig.rootDir = options.strykerOptions.basePath || process.cwd();
this.log.debug(`Project root is ${this.jestConfig.rootDir}`);
}

public async run(): Promise<RunResult> {
public async run(options: RunOptions): Promise<RunResult> {
this.setNodeEnv();

const jestTestRunner = JestTestAdapterFactory.getJestTestAdapter();

const { results } = await jestTestRunner.run(this.jestConfig, process.cwd());
const { results } = await jestTestRunner.run(this.jestConfig, process.cwd(), this.enableFindRelatedTests ? options.mutatedFileName : undefined);

// Get the non-empty errorMessages from the jest RunResult, it's safe to cast to Array<string> here because we filter the empty error messages
const errorMessages = results.testResults.map((testSuite: jest.TestResult) => testSuite.failureMessage).filter(errorMessage => (errorMessage)) as string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { Configuration, runCLI, RunResult } from 'jest';
export default class JestPromiseTestAdapter implements JestTestAdapter {
private readonly log = getLogger(JestPromiseTestAdapter.name);

public run(jestConfig: Configuration, projectRoot: string): Promise<RunResult> {
public run(jestConfig: Configuration, projectRoot: string, fileNameUnderTest?: string): Promise<RunResult> {
jestConfig.reporters = [];
const config = JSON.stringify(jestConfig);
this.log.trace(`Invoking Jest with config ${config}`);
if (fileNameUnderTest) {
this.log.trace(`Only running tests related to ${fileNameUnderTest}`);
}

return runCLI({
...(fileNameUnderTest && { _: [fileNameUnderTest], findRelatedTests: true}),
config,
runInBand: true,
silent: true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RunResult } from 'jest';

export default interface JestTestAdapter {
run(config: object, projectRoot: string): Promise<RunResult>;
run(config: object, projectRoot: string, fileNameUnderTest?: string): Promise<RunResult>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config } from 'stryker-api/config';
import { RunnerOptions, RunStatus, TestStatus } from 'stryker-api/test_runner';
import { RunnerOptions, RunStatus, TestStatus, RunOptions } from 'stryker-api/test_runner';
import * as sinon from 'sinon';
import { expect } from 'chai';
import * as path from 'path';
Expand All @@ -21,9 +21,11 @@ describe('Integration test for Strykers Jest runner', () => {
// Set timeout for integration tests to 10 seconds for travis

let jestConfigEditor: JestConfigEditor;
let runOptions: RunnerOptions;
let runnerOptions: RunnerOptions;
let processCwdStub: sinon.SinonStub;

const runOptions: RunOptions = { timeout: 0 };

// Names of the tests in the example projects
const testNames = [
'Add should be able to add two numbers',
Expand All @@ -39,7 +41,7 @@ describe('Integration test for Strykers Jest runner', () => {

jestConfigEditor = new JestConfigEditor();

runOptions = {
runnerOptions = {
fileNames: [],
port: 0,
strykerOptions: new Config()
Expand All @@ -48,11 +50,11 @@ describe('Integration test for Strykers Jest runner', () => {

it('should run tests on the example React + TypeScript project', async () => {
processCwdStub.returns(getProjectRoot('reactTsProject'));
runOptions.strykerOptions.set({ jest: { projectType: 'react-ts' } });
jestConfigEditor.edit(runOptions.strykerOptions as Config);
runnerOptions.strykerOptions.set({ jest: { projectType: 'react-ts' } });
jestConfigEditor.edit(runnerOptions.strykerOptions as Config);

const jestTestRunner = new JestTestRunner(runOptions);
const result = await jestTestRunner.run();
const jestTestRunner = new JestTestRunner(runnerOptions);
const result = await jestTestRunner.run(runOptions);

expect(result.status).to.equal(RunStatus.Complete);
expect(result).to.have.property('tests');
Expand All @@ -67,10 +69,10 @@ describe('Integration test for Strykers Jest runner', () => {
it('should run tests on the example custom project using package.json', async () => {
processCwdStub.returns(getProjectRoot('exampleProject'));

jestConfigEditor.edit(runOptions.strykerOptions as Config);
const jestTestRunner = new JestTestRunner(runOptions);
jestConfigEditor.edit(runnerOptions.strykerOptions as Config);
const jestTestRunner = new JestTestRunner(runnerOptions);

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result.errorMessages, `Errors were: ${result.errorMessages}`).lengthOf(0);
expect(result).to.have.property('tests');
Expand All @@ -89,10 +91,10 @@ describe('Integration test for Strykers Jest runner', () => {
it('should run tests on the example custom project using jest.config.js', async () => {
processCwdStub.returns(getProjectRoot('exampleProjectWithExplicitJestConfig'));

jestConfigEditor.edit(runOptions.strykerOptions as Config);
const jestTestRunner = new JestTestRunner(runOptions);
jestConfigEditor.edit(runnerOptions.strykerOptions as Config);
const jestTestRunner = new JestTestRunner(runnerOptions);

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result.errorMessages, `Errors were: ${result.errorMessages}`).lengthOf(0);
expect(result).to.have.property('tests');
Expand Down
19 changes: 10 additions & 9 deletions packages/stryker-jest-runner/test/unit/JestTestRunnerSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Config } from 'stryker-api/config';
import * as fakeResults from '../helpers/testResultProducer';
import * as sinon from 'sinon';
import { assert, expect } from 'chai';
import { RunStatus, TestStatus } from 'stryker-api/test_runner';
import { RunStatus, TestStatus, RunOptions } from 'stryker-api/test_runner';
import currentLogMock from '../helpers/logMock';

describe('JestTestRunner', () => {
const basePath = '/path/to/project/root';
const runOptions: RunOptions = { timeout: 0 };

let jestTestAdapterFactoryStub: sinon.SinonStub;
let runJestStub: sinon.SinonStub;
Expand Down Expand Up @@ -44,21 +45,21 @@ describe('JestTestRunner', () => {
});

it('should call jestTestAdapterFactory "getJestTestAdapter" method to obtain a testRunner', async () => {
await jestTestRunner.run();
await jestTestRunner.run(runOptions);

assert(jestTestAdapterFactoryStub.called);
});

it('should call the run function with the provided config and the projectRoot', async () => {
await jestTestRunner.run();
await jestTestRunner.run(runOptions);

assert(runJestStub.called);
});

it('should call the jestTestRunner run method and return a correct runResult', async () => {
runJestStub.resolves({ results: fakeResults.createSuccessResult() });

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result).to.deep.equal({
errorMessages: [],
Expand All @@ -77,7 +78,7 @@ describe('JestTestRunner', () => {
it('should call the jestTestRunner run method and return a skipped runResult', async () => {
runJestStub.resolves({ results: fakeResults.createPendingResult() });

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result).to.deep.equal({
errorMessages: [],
Expand All @@ -96,7 +97,7 @@ describe('JestTestRunner', () => {
it('should call the jestTestRunner run method and return a negative runResult', async () => {
runJestStub.resolves({ results: fakeResults.createFailResult() });

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result).to.deep.equal({
errorMessages: ['test failed - App.test.js'],
Expand Down Expand Up @@ -131,7 +132,7 @@ describe('JestTestRunner', () => {
it('should return an error result when a runtime error occurs', async () => {
runJestStub.resolves({ results: { testResults: [], numRuntimeErrorTestSuites: 1 } });

const result = await jestTestRunner.run();
const result = await jestTestRunner.run(runOptions);

expect(result).to.deep.equal({
errorMessages: [],
Expand All @@ -141,15 +142,15 @@ describe('JestTestRunner', () => {
});

it('should set process.env.NODE_ENV to \'test\' when process.env.NODE_ENV is null', async () => {
await jestTestRunner.run();
await jestTestRunner.run(runOptions);

expect(processEnvMock.NODE_ENV).to.equal('test');
});

it('should keep the value set in process.env.NODE_ENV if not null', async () => {
processEnvMock.NODE_ENV = 'stryker';

await jestTestRunner.run();
await jestTestRunner.run(runOptions);

expect(processEnvMock.NODE_ENV).to.equal('stryker');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('JestPromiseTestAdapter', () => {
let runCLIStub: sinon.SinonStub;

const projectRoot = '/path/to/project';
const fileNameUnderTest = '/path/to/file';
const jestConfig: any = { rootDir: projectRoot };

beforeEach(() => {
Expand Down Expand Up @@ -37,6 +38,18 @@ describe('JestPromiseTestAdapter', () => {
}, [projectRoot]));
});

it('should call the runCLI method with the --findRelatedTests flag', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Great test!

await jestPromiseTestAdapter.run(jestConfig, projectRoot, fileNameUnderTest);

assert(runCLIStub.calledWith({
_: [fileNameUnderTest],
config: JSON.stringify({ rootDir: projectRoot, reporters: [] }),
findRelatedTests: true,
runInBand: true,
silent: true
}, [projectRoot]));
});

it('should call the runCLI method and return the test result', async () => {
const result = await jestPromiseTestAdapter.run(jestConfig, projectRoot);

Expand All @@ -50,6 +63,21 @@ describe('JestPromiseTestAdapter', () => {
});
});

it('should call the runCLI method and return the test result when run with --findRelatedTests flag', async () => {
const result = await jestPromiseTestAdapter.run(jestConfig, projectRoot, fileNameUnderTest);

expect(result).to.deep.equal({
config: {
nicojs marked this conversation as resolved.
Show resolved Hide resolved
_: [fileNameUnderTest],
config: JSON.stringify({ rootDir: projectRoot, reporters: [] }),
findRelatedTests: true,
runInBand: true,
silent: true
},
result: 'testResult'
});
});

it('should trace log a message when jest is invoked', async () => {
await jestPromiseTestAdapter.run(jestConfig, projectRoot);

Expand Down
4 changes: 3 additions & 1 deletion packages/stryker-jest-runner/types/jest/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ declare namespace Jest {
config: string;
runInBand: boolean;
silent: boolean;
findRelatedTests?: boolean;
_?: string[];
}

// Taken from https://goo.gl/qHifyP, removed all stuff that we are not using
Expand All @@ -18,7 +20,7 @@ declare namespace Jest {
collectCoverage: boolean;
verbose: boolean;
testResultsProcessor: Maybe<string>;
testEnvironment: string
testEnvironment: string;
}

interface RunResult {
Expand Down
6 changes: 3 additions & 3 deletions packages/stryker/src/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default class Sandbox {
return sandbox.initialize().then(() => sandbox);
}

public run(timeout: number, testHooks: string | undefined): Promise<RunResult> {
return this.testRunner.run({ timeout, testHooks });
public run(timeout: number, testHooks: string | undefined, mutatedFileName?: string): Promise<RunResult> {
return this.testRunner.run({ timeout, testHooks, mutatedFileName });
}

public dispose(): Promise<void> {
Expand All @@ -58,7 +58,7 @@ export default class Sandbox {
this.log.warn(`Failed find coverage data for this mutant, running all tests. This might have an impact on performance: ${transpiledMutant.mutant.toString()}`);
}
await Promise.all(mutantFiles.map(mutatedFile => this.writeFileInSandbox(mutatedFile)));
const runResult = await this.run(this.calculateTimeout(transpiledMutant.mutant), this.getFilterTestsHooks(transpiledMutant.mutant));
const runResult = await this.run(this.calculateTimeout(transpiledMutant.mutant), this.getFilterTestsHooks(transpiledMutant.mutant), this.fileMap[transpiledMutant.mutant.fileName]);
await this.reset(mutantFiles);
return runResult;
}
Expand Down
21 changes: 18 additions & 3 deletions packages/stryker/test/unit/SandboxSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,19 @@ describe('Sandbox', () => {
const sut = await Sandbox.create(options, SANDBOX_INDEX, files, null, 0, LOGGING_CONTEXT);
await sut.run(231313, 'hooks');
expect(testRunner.run).to.have.been.calledWith({
mutatedFileName: undefined,
testHooks: 'hooks',
timeout: 231313
timeout: 231313,
});
});

it('should run the testRunner with mutatedFileName', async () => {
const sut = await Sandbox.create(options, SANDBOX_INDEX, files, null, 0, LOGGING_CONTEXT);
await sut.run(231313, 'hooks', 'path/to/file');
expect(testRunner.run).to.have.been.calledWith({
mutatedFileName: 'path/to/file',
testHooks: 'hooks',
timeout: 231313,
});
});
});
Expand Down Expand Up @@ -196,13 +207,17 @@ describe('Sandbox', () => {
expect(testFrameworkStub.filter).to.have.been.calledWith(transpiledMutant.mutant.selectedTests);
});

it('should provide the filter code as testHooks and correct timeout', async () => {
it('should provide the filter code as testHooks, correct timeout and mutatedFileName', async () => {
options.timeoutMS = 1000;
const overheadTimeMS = 42;
const totalTimeSpend = 12;
const sut = await Sandbox.create(options, SANDBOX_INDEX, files, testFrameworkStub, overheadTimeMS, LOGGING_CONTEXT);
await sut.runMutant(transpiledMutant);
const expectedRunOptions = { testHooks: wrapInClosure(testFilterCodeFragment), timeout: totalTimeSpend * options.timeoutFactor + options.timeoutMS + overheadTimeMS };
const expectedRunOptions = {
mutatedFileName: path.resolve('random-folder-3', 'file1'),
testHooks: wrapInClosure(testFilterCodeFragment),
timeout: totalTimeSpend * options.timeoutFactor + options.timeoutMS + overheadTimeMS
};
expect(testRunner.run).calledWith(expectedRunOptions);
});

Expand Down