Skip to content

Commit

Permalink
feat(mocha-runner): support mocha 8 (#2259)
Browse files Browse the repository at this point in the history
* Support peer dependency range mocha@<9
* Add support for [rootHooks](mochajs/mocha#4244)
* Ignore `--parallel` flag (stryker will handle concurrency).
  • Loading branch information
nicojs committed Jun 16, 2020
1 parent f440089 commit 917d965
Show file tree
Hide file tree
Showing 21 changed files with 2,635 additions and 3,394 deletions.
5,827 changes: 2,486 additions & 3,341 deletions e2e/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e2e/package.json
Expand Up @@ -27,7 +27,7 @@
"karma-webpack": "^4.0.2",
"link-parent-bin": "~1.0.0",
"load-grunt-tasks": "~5.1.0",
"mocha": "~7.1.1",
"mocha": "^8.0.1",
"mutation-testing-metrics": "~1.3.0",
"rxjs": "~6.5.3",
"semver": "~6.3.0",
Expand Down
5 changes: 0 additions & 5 deletions e2e/test/babel-transpiling/package-lock.json

This file was deleted.

1 change: 0 additions & 1 deletion e2e/test/babel-transpiling/package.lock.json

This file was deleted.

4 changes: 4 additions & 0 deletions e2e/test/mocha-mocha/.mocharc.jsonc
@@ -0,0 +1,4 @@
{
"require": "./test/helpers/testSetup.js",
"spec": ["test/unit/*.js"]
}
3 changes: 0 additions & 3 deletions e2e/test/mocha-mocha/stryker.conf.js
Expand Up @@ -5,8 +5,5 @@ module.exports = function (config) {
testRunner: 'mocha',
reporters: ['clear-text', 'html', 'event-recorder'],
maxConcurrentTestRunners: 2,
mochaOptions: {
spec: ['test/*.js', 'helpers/*.js']
},
});
};
5 changes: 5 additions & 0 deletions e2e/test/mocha-mocha/test/helpers/testSetup.js
@@ -0,0 +1,5 @@
exports.mochaHooks = {
beforeAll() {
global.expect = require('chai').expect;
}
}
@@ -1,5 +1,4 @@
var expect = require('chai').expect;
var addModule = require('../src/Add');
var addModule = require('../../src/Add');
var add = addModule.add;
var addOne = addModule.addOne;
var isNegativeNumber = addModule.isNegativeNumber;
Expand Down
@@ -1,5 +1,4 @@
var expect = require('chai').expect;
var circleModule = require('../src/Circle');
var circleModule = require('../../src/Circle');
var getCircumference = circleModule.getCircumference;

describe('Circle', function() {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -33,7 +33,7 @@
"json-schema-to-typescript": "~9.1.0",
"lerna": "^3.10.7",
"link-parent-bin": "~1.0.0",
"mocha": "^6.1.2",
"mocha": "^8.0.1",
"nyc": "^15.0.0",
"prettier": "2.0.5",
"rimraf": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/mocha-runner/package.json
Expand Up @@ -50,6 +50,6 @@
},
"peerDependencies": {
"@stryker-mutator/core": "^3.0.0",
"mocha": ">= 2.3.3 < 8"
"mocha": ">= 2.3.3 < 9"
}
}
10 changes: 9 additions & 1 deletion packages/mocha-runner/src/LibWrapper.ts
Expand Up @@ -5,6 +5,8 @@ import { MochaOptions } from '../src-generated/mocha-runner-options';

let loadOptions: undefined | ((argv?: string[] | string) => { [key: string]: any } | undefined);
let handleFiles: undefined | ((options: MochaOptions) => string[]);
let handleRequires: undefined | ((requires?: string[]) => Promise<any>);
let loadRootHooks: undefined | ((rootHooks: any) => Promise<any>);

try {
/*
Expand All @@ -19,7 +21,11 @@ try {

try {
// https://github.com/mochajs/mocha/blob/master/lib/cli/run-helpers.js#L132
handleFiles = require('mocha/lib/cli/run-helpers').handleFiles;
const runHelpers = require('mocha/lib/cli/run-helpers');
handleFiles = runHelpers.handleFiles;
handleRequires = runHelpers.handleRequires; // handleRequires is available since mocha v7.2
loadRootHooks = runHelpers.loadRootHooks; // loadRootHooks is available since mocha v7.2

if (!handleFiles) {
// Might be moved: https://github.com/mochajs/mocha/commit/15b96afccaf508312445770e3af1c145d90b28c6#diff-39b692a81eb0c9f3614247af744ab4a8
handleFiles = require('mocha/lib/cli/collect-files');
Expand All @@ -37,4 +43,6 @@ export default class LibWrapper {
public static multimatch = multimatch;
public static loadOptions = loadOptions;
public static handleFiles = handleFiles;
public static handleRequires = handleRequires;
public static loadRootHooks = loadRootHooks;
}
20 changes: 14 additions & 6 deletions packages/mocha-runner/src/MochaTestRunner.ts
Expand Up @@ -18,22 +18,23 @@ const DEFAULT_TEST_PATTERN = 'test/**/*.js';
export class MochaTestRunner implements TestRunner {
private testFileNames: string[];
private readonly mochaOptions: MochaOptions;
private rootHooks: any;

public static inject = tokens(commonTokens.logger, commonTokens.sandboxFileNames, commonTokens.options);
constructor(private readonly log: Logger, private readonly allFileNames: readonly string[], options: StrykerOptions) {
this.mochaOptions = (options as MochaRunnerWithStrykerOptions).mochaOptions;
this.additionalRequires();
StrykerMochaReporter.log = log;
}

public init(): void {
public async init(): Promise<void> {
if (LibWrapper.handleFiles) {
this.log.debug("Mocha >= 6 detected. Using mocha's `handleFiles` to load files");
this.testFileNames = this.mocha6DiscoverFiles(LibWrapper.handleFiles);
} else {
this.log.debug('Mocha < 6 detected. Using custom logic to discover files');
this.testFileNames = this.legacyDiscoverFiles();
}
await this.additionalRequires();
}

private mocha6DiscoverFiles(handleFiles: (options: MochaOptions) => string[]): string[] {
Expand Down Expand Up @@ -92,7 +93,7 @@ export class MochaTestRunner implements TestRunner {
return new Promise<RunResult>((resolve, reject) => {
try {
this.purgeFiles();
const mocha = new LibWrapper.Mocha({ reporter: StrykerMochaReporter as any, bail: true });
const mocha = new LibWrapper.Mocha({ reporter: StrykerMochaReporter as any, bail: true, rootHooks: this.rootHooks } as Mocha.MochaOptions);
this.configure(mocha);
this.addTestHooks(mocha, testHooks);
this.addFiles(mocha);
Expand Down Expand Up @@ -162,10 +163,17 @@ export class MochaTestRunner implements TestRunner {
}
}

private additionalRequires() {
private async additionalRequires() {
if (this.mochaOptions.require) {
const modulesToRequire = this.mochaOptions.require.map((moduleName) => (moduleName.startsWith('.') ? path.resolve(moduleName) : moduleName));
modulesToRequire.forEach(LibWrapper.require);
if (LibWrapper.handleRequires) {
const rawRootHooks = await LibWrapper.handleRequires(this.mochaOptions.require);
if (rawRootHooks) {
this.rootHooks = await LibWrapper.loadRootHooks!(rawRootHooks);
}
} else {
const modulesToRequire = this.mochaOptions.require.map((moduleName) => (moduleName.startsWith('.') ? path.resolve(moduleName) : moduleName));
modulesToRequire.forEach(LibWrapper.require);
}
}
}
}
Expand Up @@ -25,7 +25,7 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
expect(actualConfig).deep.eq({
...DEFAULT_MOCHA_OPTIONS,
config: configFile,
opts: false, // mocha sets opts: false after loading it...
opts: './test/mocha.opts',
package: false, // mocha sets package: false after loading it...
extension: ['js'],
timeout: 2000,
Expand All @@ -38,7 +38,7 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
const actualConfig = actLoad({ config: configFile });
expect(actualConfig).deep.eq({
...DEFAULT_MOCHA_OPTIONS,
opts: false, // mocha sets opts: false after loading it...
opts: './test/mocha.opts',
package: false, // mocha sets package: false after loading it...
config: configFile,
extension: ['json', 'js'],
Expand All @@ -53,7 +53,7 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
expect(actualConfig).deep.eq({
...DEFAULT_MOCHA_OPTIONS,
config: configFile,
opts: false, // mocha sets opts: false after loading it...
opts: './test/mocha.opts',
package: false, // mocha sets package: false after loading it...
extension: ['jsonc', 'js'],
timeout: 2000,
Expand All @@ -68,7 +68,7 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
...DEFAULT_MOCHA_OPTIONS,
'async-only': false,
config: configFile,
opts: false, // mocha sets opts: false after loading it...
opts: './test/mocha.opts',
package: false, // mocha sets package: false after loading it...
extension: ['yml', 'js'],
file: ['/path/to/some/file', '/path/to/some/other/file'],
Expand All @@ -80,24 +80,6 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
});
});

it('should support loading from "mocha.opts" (including providing files)', () => {
const configFile = resolveMochaConfig('mocha.opts');
const actualConfig = actLoad({ opts: configFile });
expect(actualConfig).deep.eq({
...DEFAULT_MOCHA_OPTIONS,
'async-only': true,
extension: ['js'],
config: false, // mocha sets config: false after loading it...
package: false, // mocha sets package: false after loading it...
file: [],
ignore: [],
opts: configFile,
spec: ['/tests/**/*.js', '/foo/*.js'],
timeout: 2000,
ui: 'bdd',
});
});

it('should support loading from "package.json"', () => {
const pkgFile = resolveMochaConfig('package.json');
const actualConfig = actLoad({ package: pkgFile });
Expand Down Expand Up @@ -128,7 +110,7 @@ describe(`${MochaOptionsLoader.name} integration`, () => {
'no-opts': true,
});
const expectedOptions = {
extension: ['js'],
extension: ['js', 'cjs', 'mjs'],
['no-config']: true,
['no-opts']: true,
['no-package']: true,
Expand Down
@@ -0,0 +1,34 @@
import * as path from 'path';

import { commonTokens } from '@stryker-mutator/api/plugin';
import { testInjector } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';
import { RunStatus } from '@stryker-mutator/api/test_runner';

import { MochaTestRunner } from '../../src/MochaTestRunner';
import MochaOptionsEditor from '../../src/MochaOptionsEditor';
import MochaOptionsLoader from '../../src/MochaOptionsLoader';

describe('Running a project with root hooks', () => {
const cwd = process.cwd();

let sut: MochaTestRunner;

beforeEach(async () => {
process.chdir(path.resolve(__dirname, '..', '..', 'testResources', 'parallel-with-root-hooks-sample'));
testInjector.injector.provideClass('loader', MochaOptionsLoader).injectClass(MochaOptionsEditor).edit(testInjector.options);
sut = testInjector.injector.provideValue(commonTokens.sandboxFileNames, []).injectClass(MochaTestRunner);
await sut.init();
});

afterEach(() => {
process.chdir(cwd);
});

it('should have run the root hooks', async () => {
const result = await sut.run({});
expect(result.status).eq(RunStatus.Complete);
expect(result.tests).has.lengthOf(2);
expect(result.errorMessages).lengthOf(0);
});
});
6 changes: 6 additions & 0 deletions packages/mocha-runner/test/unit/MochaOptionsLoader.spec.ts
Expand Up @@ -106,6 +106,12 @@ describe(MochaOptionsLoader.name, () => {
const actualOptions = sut.load(options);
expect(actualOptions).deep.eq(createMochaOptions());
});

it('should not allow to set parallel', () => {
rawOptions['parallel'] = true;
const actualOptions = sut.load(options);
expect((actualOptions as any).parallel).undefined;
});
});

describe('with mocha < 6', () => {
Expand Down
48 changes: 41 additions & 7 deletions packages/mocha-runner/test/unit/MochaTestRunner.spec.ts
Expand Up @@ -20,11 +20,15 @@ describe(MochaTestRunner.name, () => {
let sut: MochaTestRunner;
let requireStub: sinon.SinonStub;
let handleFilesStub: sinon.SinonStub;
let handleRequiresStub: sinon.SinonStub;
let loadRootHooks: sinon.SinonStub;

beforeEach(() => {
MochaStub = sinon.stub(LibWrapper, 'Mocha');
requireStub = sinon.stub(LibWrapper, 'require');
handleFilesStub = sinon.stub(LibWrapper, 'handleFiles');
handleRequiresStub = sinon.stub(LibWrapper, 'handleRequires');
loadRootHooks = sinon.stub(LibWrapper, 'loadRootHooks');
sinon.stub(utils, 'evalGlobal');
mocha = sinon.createStubInstance(Mocha) as any;
mocha.suite = sinon.createStubInstance(EventEmitter) as Mocha.Suite & sinon.SinonStubbedInstance<EventEmitter>;
Expand Down Expand Up @@ -108,7 +112,7 @@ describe(MochaTestRunner.name, () => {
actAssertMatchedPatterns(undefined, expectedGlobPatterns);
});

it('should throw an error if no files could be discovered', () => {
it('should reject if no files could be discovered', async () => {
// Arrange
multimatchStub.returns([]);
const files = ['foo.js', 'bar.js'];
Expand All @@ -118,10 +122,10 @@ describe(MochaTestRunner.name, () => {

// Act
sut = createSut({ fileNames: files });
const actFn = () => sut.init();
const onGoingWork = sut.init();

// Assert
expect(actFn).throws(
await expect(onGoingWork).rejectedWith(
`[MochaTestRunner] No files discovered (tried pattern(s) ${relativeGlobbing}). Please specify the files (glob patterns) containing your tests in mochaOptions.spec in your config file.`
);
expect(testInjector.logger.debug).calledWith(`Tried ${absoluteGlobbing} on files: ${filesStringified}.`);
Expand Down Expand Up @@ -199,17 +203,21 @@ describe(MochaTestRunner.name, () => {
expect(mocha.asyncOnly).not.called;
});

it('should pass require additional require options when constructed', () => {
it('should pass require additional require options when constructed', async () => {
handleRequiresStub.value(undefined);
const mochaOptions: Partial<MochaOptions> = { require: ['ts-node', 'babel-register'] };
createSut({ mochaOptions });
sut = createSut({ mochaOptions });
await sut.init();
expect(requireStub).calledTwice;
expect(requireStub).calledWith('ts-node');
expect(requireStub).calledWith('babel-register');
});

it('should pass and resolve relative require options when constructed', () => {
it('should pass and resolve relative require options when constructed', async () => {
handleRequiresStub.value(undefined);
const mochaOptions: Partial<MochaOptions> = { require: ['./setup.js', 'babel-register'] };
createSut({ mochaOptions });
sut = createSut({ mochaOptions });
await sut.init();
const resolvedRequire = path.resolve('./setup.js');
expect(requireStub).calledTwice;
expect(requireStub).calledWith(resolvedRequire);
Expand Down Expand Up @@ -246,6 +254,32 @@ describe(MochaTestRunner.name, () => {
});
});

describe('when mocha version >=8', () => {
beforeEach(() => {
handleFilesStub.returns(['src/math.js', 'test/math.spec.js']);
});

it('should handle require and allow for rootHooks', async () => {
handleRequiresStub.resolves(['root-hook1', 'bar-hook']);
const mochaOptions: Partial<MochaOptions> = { require: ['./setup.js', 'babel-register'] };
sut = createSut({ mochaOptions });
await sut.init();
expect(handleRequiresStub).calledWithExactly(['./setup.js', 'babel-register']);
expect(loadRootHooks).calledWithExactly(['root-hook1', 'bar-hook']);
});

it('should pass rootHooks to the mocha instance', async () => {
handleRequiresStub.resolves(['root-hook1', 'bar-hook']);
const rootHooks = { beforeEach() {} };
loadRootHooks.resolves(rootHooks);
const mochaOptions: Partial<MochaOptions> = { require: ['./setup.js', 'babel-register'] };
sut = createSut({ mochaOptions });
await sut.init();
await actRun();
expect(LibWrapper.Mocha).calledWithMatch({ rootHooks });
});
});

async function actRun(options: RunOptions = { timeout: 0 }) {
mocha.run.callsArg(0);
return sut.run(options);
Expand Down
@@ -0,0 +1,6 @@
{
"ui": "bdd",
"parallel": true,
"require": "./test/setup.js",
"spec": "test/unit/*.js"
}
@@ -0,0 +1,5 @@
exports.mochaHooks = {
beforeEach() {
global.add = (a, b) => a + b;
}
}
@@ -0,0 +1,8 @@
const assert = require('assert');

describe('add', () => {
it('should add 1 + 1 = 2', () => {
assert.equal(add(1, 1), 2);
});
})

@@ -0,0 +1,7 @@
const assert = require('assert');

describe('add also', () => {
it('should add 2 - 3 = -1', () => {
assert.equal(add(2, -3), -1);
});
})

0 comments on commit 917d965

Please sign in to comment.