Skip to content
Permalink
Browse files
fix(core): refactored test runner adapters to introduce a common inte…
…rface they all implement
  • Loading branch information
jan-molak committed Feb 19, 2021
1 parent 3b16f09 commit bf82e7c
Show file tree
Hide file tree
Showing 20 changed files with 254 additions and 174 deletions.
@@ -3,9 +3,15 @@ import * as path from 'path';
import { Version } from './Version';

/**
* @package
* @desc
* Dynamically loads Node modules located relative to `cwd`.
*/
export class ModuleLoader {

/**
* @param {string} cwd
* Current working directory, relative to which Node modules should be resolved.
*/
constructor(public readonly cwd: string) {
}

@@ -25,10 +31,14 @@ export class ModuleLoader {
}

/**
* @package
* @desc
* Works like `require.resolve`, but relative to specified current working directory
*
* @param moduleId
* NPM module id, for example 'cucumber' or '@serenity-js/core'
* @param {string} moduleId
* NPM module id, for example `cucumber` or `@serenity-js/core`
*
* @returns {string}
* Path a given Node module
*/
resolve(moduleId: string): string {
const fromFile = path.join(this.cwd, 'noop.js');
@@ -40,6 +50,14 @@ export class ModuleLoader {
});
}

/**
* @desc
* Works like `require`, but relative to specified current working directory
*
* @param {string} moduleId
*
* @returns {any}
*/
require(moduleId: string): any {
try {
return require(this.resolve(moduleId));
@@ -49,6 +67,13 @@ export class ModuleLoader {
}
}

/**
* @desc
* Returns {@link Version} of module specified by `moduleId`, based on its `package.json`.
*
* @param {string} moduleId
* @returns {Version}
*/
versionOf(moduleId: string): Version {
return new Version(this.require(`${ moduleId }/package.json`).version);
}
@@ -0,0 +1,17 @@
/**
* @desc
* Describes an adapter needed to run a given type of tests programmatically
*
* @interface
*/
export interface TestRunnerAdapter {

/**
* @desc
* Runs test scenarios at pathsToScenarios using the given test runner.
*
* @abstract
* @type {function(pathsToScenarios: string[]): Promise<void>}
*/
run: (pathsToScenarios: string[]) => Promise<void>;
}
@@ -1,30 +1,57 @@
import semver = require('semver');
import { ensure, isDefined, isString, Predicate, TinyType } from 'tiny-types';

/**
* @desc
* A tiny type describing a version number, like `1.2.3`
*
* @extends {tiny-types~TinyType}
*/
export class Version extends TinyType {

/**
* @param {string} version
* @returns {Version}
*/
static fromJSON(version: string) {
return new Version(version);
}

/**
* @param {string} version
*/
constructor(private readonly version: string) {
super();
ensure('version', version, isDefined(), isString(), isValid());
}

isAtLeast(other: Version) {
/**
* @param {Version} other
* @returns {boolean}
*/
isAtLeast(other: Version): boolean {
return semver.gte(this.version, other.version);
}

/**
* @returns {number}
* Major version number of a given package version, i.e. `1` in `1.2.3`
*/
major(): number {
return Number(this.version.split('.')[0]);
}

toString() {
/**
* @returns {string}
*/
toString(): string {
return `${ this.version }`;
}
}

/**
* @package
*/
function isValid(): Predicate<string> {
return Predicate.to(`be a valid version number`, (version: string) =>
!! semver.valid(version),
@@ -10,5 +10,6 @@ export * from './FileSystemLocation';
export * from './formatted';
export * from './ModuleLoader';
export * from './Path';
export * from './TestRunnerAdapter';
export * from './trimmed';
export * from './Version';
@@ -1,16 +1,24 @@
/* istanbul ignore file covered in integration tests */
import { ModuleLoader, Version } from '@serenity-js/core/lib/io';
import { ModuleLoader, TestRunnerAdapter, Version } from '@serenity-js/core/lib/io';
import { CucumberConfig } from './CucumberConfig';
import { CucumberOptions } from './CucumberOptions';
import { CucumberFormatterOutput, OutputDescriptor } from './output';

/**
* @private
* @desc
* Allows for programmatic execution of Cucumber test scenarios.
*
* @implements {@serenity-js/core/lib/io~TestRunnerAdapter}
*/
export class CucumberCLIAdapter {
export class CucumberCLIAdapter implements TestRunnerAdapter {

private readonly options: CucumberOptions;

/**
* @param {CucumberConfig} config
* @param {@serenity-js/core/lib/io~ModuleLoader} loader
* @param {CucumberFormatterOutput} output
*/
constructor(
config: CucumberConfig,
private readonly loader: ModuleLoader,
@@ -19,6 +27,15 @@ export class CucumberCLIAdapter {
this.options = new CucumberOptions(config);
}

/**
* @desc
* Instructs Cucumber to execute feature files located at `pathsToScenarios`
*
* @param {string[]} pathsToScenarios
* Absolute or relative paths to feature files
*
* @returns {Promise<void>}
*/
async run(pathsToScenarios: string[]): Promise<void> {
const version = this.loader.hasAvailable('@cucumber/cucumber')
? this.loader.versionOf('@cucumber/cucumber')
@@ -1,14 +1,20 @@
import { ModuleLoader } from '@serenity-js/core/lib/io';
import { ModuleLoader, TestRunnerAdapter } from '@serenity-js/core/lib/io';
import reporter = require('../index');
import { JasmineConfig } from './JasmineConfig';

/**
* @desc
* Allows for programmatic execution of Jasmine test scenarios,
* using {@link SerenityReporterForJasmine} to report progress.
*
* @implements {@serenity-js/core/lib/io~TestRunnerAdapter}
*/
export class JasmineAdapter {
export class JasmineAdapter implements TestRunnerAdapter {

/**
* @param {JasmineConfig} config
* @param {@serenity-js/core/lib/io~ModuleLoader} loader
*/
constructor(
private readonly config: JasmineConfig,
private readonly loader: ModuleLoader,
@@ -1,19 +1,24 @@
/* istanbul ignore file */

import * as fs from 'fs';
import * as path from 'path';
import { ModuleLoader } from '@serenity-js/core/lib/io';
import { ModuleLoader, TestRunnerAdapter } from '@serenity-js/core/lib/io';
import { MochaConfig } from './MochaConfig';

/**
* @desc
* Allows for programmatic execution of Mocha test scenarios,
* using {@link SerenityReporterForMocha} to report progress.
*
* @package
* @implements {@serenity-js/core/lib/io~TestRunnerAdapter}
*/
export class MochaAdapter {
export class MochaAdapter implements TestRunnerAdapter {

/**
* @desc
* test
* @param {MochaConfig} config
* @param {@serenity-js/core/lib/io~ModuleLoader} loader
*/
constructor(
private readonly config: MochaConfig,
private readonly loader: ModuleLoader,
@@ -3,14 +3,13 @@ import 'mocha';
import { expect } from '@integration/testing-tools';
import { Serenity } from '@serenity-js/core';
import { SceneFinished, SceneFinishes, SceneStarts } from '@serenity-js/core/lib/events';
import { FileSystemLocation, Path } from '@serenity-js/core/lib/io';
import { FileSystemLocation, Path, TestRunnerAdapter } from '@serenity-js/core/lib/io';
import { Category, CorrelationId, ExecutionFailedWithError, ExecutionSuccessful, Name, ProblemIndication, ScenarioDetails } from '@serenity-js/core/lib/model';
import { ArtifactArchiver, Clock, StageCrewMember } from '@serenity-js/core/lib/stage';
import { Config, Runner } from 'protractor';
import * as sinon from 'sinon';

import { ProtractorFrameworkAdapter, TestRunnerDetector } from '../../src/adapter';
import { TestRunner } from '../../src/adapter/runners/TestRunner';

describe('ProtractorFrameworkAdapter', () => {

@@ -24,7 +23,7 @@ describe('ProtractorFrameworkAdapter', () => {

let protractorRunner: sinon.SinonStubbedInstance<Runner>,
testRunnerDetector: sinon.SinonStubbedInstance<TestRunnerDetector>,
testRunner: TestRunner,
testRunnerAdapter: TestRunnerAdapter,
serenity: Serenity,
adapter: ProtractorFrameworkAdapter;

@@ -41,9 +40,9 @@ describe('ProtractorFrameworkAdapter', () => {
protractorRunner = sinon.createStubInstance(Runner);
testRunnerDetector = sinon.createStubInstance(TestRunnerDetector);
serenity = new Serenity(discreteClock);
testRunner = new SimpleTestRunner(serenity);
testRunnerAdapter = new SimpleTestRunnerAdapter(serenity);

testRunnerDetector.runnerFor.returns(testRunner);
testRunnerDetector.runnerFor.returns(testRunnerAdapter);

adapter = new ProtractorFrameworkAdapter(serenity, protractorRunner, testRunnerDetector as unknown as TestRunnerDetector);
});
@@ -129,7 +128,7 @@ describe('ProtractorFrameworkAdapter', () => {
});

it('invokes runner.runTestPreparer before executing the tests', () => {
const testRunnerRunMethod = sinon.spy(testRunner, 'run');
const testRunnerRunMethod = sinon.spy(testRunnerAdapter, 'run');

return expect(adapter.run([])).to.be.fulfilled
.then(() => {
@@ -148,7 +147,7 @@ describe('ProtractorFrameworkAdapter', () => {

protractorRunner.getConfig.returns(protractorConfig);

const testRunnerRunMethod = sinon.spy(testRunner, 'run');
const testRunnerRunMethod = sinon.spy(testRunnerAdapter, 'run');

return expect(adapter.run([])).to.be.fulfilled
.then(() => {
@@ -283,7 +282,7 @@ describe('ProtractorFrameworkAdapter', () => {
});
});

class SimpleTestRunner implements TestRunner {
class SimpleTestRunnerAdapter implements TestRunnerAdapter {

constructor(private readonly serenityInstance: Serenity) {
}

0 comments on commit bf82e7c

Please sign in to comment.