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(mocha-runner): support mocha 8 #2259

Merged
merged 3 commits into from Jun 16, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
});
})