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

Replace mockery to sinon library in mock-run which is used for Tasks Unit Tests #961

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
109 changes: 91 additions & 18 deletions node/mock-run.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import ma = require('./mock-answer');
import mockery = require('mockery');
import { TaskLibAnswers } from './mock-answer';
import { SinonSandbox, createSandbox } from 'sinon';
import im = require('./internal');
import * as taskLib from './task';

export class TaskMockRunner {
constructor(taskPath: string) {
this._taskPath = taskPath;
this._sandbox = createSandbox();
}

_taskPath: string;
_answers: ma.TaskLibAnswers | undefined;
_answers: TaskLibAnswers | undefined;
_exports: {[key: string]: any} = { };
_moduleCount: number = 0;
private _sandbox: SinonSandbox;

public setInput(name: string, val: string) {
let key: string = im._getVariableKey(name);
Expand All @@ -32,21 +35,81 @@ export class TaskMockRunner {
*
* @param answers Answers to be returned when the task lib functions are called.
*/
public setAnswers(answers: ma.TaskLibAnswers) {
public setAnswers(answers: TaskLibAnswers) {
this._answers = answers;
}
/**
* Checks if a module name is valid for import, avoiding local module references.
*
* @param {string} modName - The name of the module to be checked.
* @returns {boolean} Returns true if the module name is valid, otherwise false.
*/
checkModuleName(modName: string): boolean {
if (modName.includes('.')) {
console.error(`ERROR: ${modName} is a local module. Cannot import it from task-lib. Please pass full path.`);
return false;
}
return true;
}

/**
* Checks if a method in a new module is mockable based on specified conditions.
*
* @param {object} newModule - The new module containing the method.
* @param {string} methodName - The name of the method to check.
* @param {object} oldModule - The original module from which the method might be inherited.
* @returns {boolean} Returns true if the method is mockable, otherwise false.
*/
checkIsMockable(newModule, methodName, oldModule) {
// Get the method from the newModule
const method = newModule[methodName];

// Check if the method exists and is not undefined
if (!newModule.hasOwnProperty(methodName) || typeof method === 'undefined') {
return false;
}

// Check if the method is a function
if (typeof method !== 'function') {
console.log(`WARNING: ${methodName} of ${newModule} is not a function. There is no option to replace getter/setter in this implementation. You can consider changing it.`);
LiliaSabitova marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

// Check if the method is writable
const descriptor = Object.getOwnPropertyDescriptor(oldModule, methodName);
return descriptor && descriptor.writable !== false;
}

/**
* Register a mock module. When require() is called for the module name,
* the mock implementation will be returned instead.
* Registers a mock module, allowing the replacement of methods with mock implementations.
*
* @param modName Module name to override.
* @param val Mock implementation of the module.
* @returns void
* @param {string} modName - The name of the module to be overridden.
* @param {object} modMock - The mock implementation of the module.
* @returns {void}
*/
public registerMock(modName: string, mod: any): void {
public registerMock(modName: string, modMock: object): void {
this._moduleCount++;
mockery.registerMock(modName, mod);
let oldMod: object;

// Check if the module name is valid and can be imported
if (this.checkModuleName(modName)) {
oldMod = require(modName);
} else {
console.error(`ERROR: Cannot import ${modName}.`);
return;
}

// Iterate through methods in the old module and replace them with mock implementations
for (let method in oldMod) {
if (this.checkIsMockable(modMock, method, oldMod)) {
const replacement = modMock[method] || oldMod[method];
try {
this._sandbox.replace(oldMod, method, replacement);
} catch (error) {
console.error('ERROR: Cannot replace ${method} in ${oldMod} by ${replacement}. ${error.message}', );
}
}
}
}

/**
Expand All @@ -69,11 +132,6 @@ export class TaskMockRunner {
* @returns void
*/
public run(noMockTask?: boolean): void {
// determine whether to enable mockery
if (!noMockTask || this._moduleCount) {
mockery.enable({warnOnUnregistered: false});
}

// answers and exports not compatible with "noMockTask" mode
if (noMockTask) {
if (this._answers || Object.keys(this._exports).length) {
Expand All @@ -82,7 +140,7 @@ export class TaskMockRunner {
}
// register mock task lib
else {
var tlm = require('azure-pipelines-task-lib/mock-task');
let tlm = require('azure-pipelines-task-lib/mock-task');
if (this._answers) {
tlm.setAnswers(this._answers);
}
Expand All @@ -92,10 +150,25 @@ export class TaskMockRunner {
tlm[key] = this._exports[key];
});

mockery.registerMock('azure-pipelines-task-lib/task', tlm);
// With sinon we have to iterate through methods in the old module and replace them with mock implementations
let tlt = require('azure-pipelines-task-lib/task');
for (let method in tlt) {
LiliaSabitova marked this conversation as resolved.
Show resolved Hide resolved
if (tlm.hasOwnProperty(method)) {
this._sandbox.replace(tlt, method, tlm[method]);
}
}

}

// run it
require(this._taskPath);
}
/**
* Restores the sandboxed environment to its original state.
*
* @returns {void}
*/
public restore() {
this._sandbox.restore();
}
}
124 changes: 122 additions & 2 deletions node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"q": "^1.5.1",
"semver": "^5.1.0",
"shelljs": "^0.8.5",
"sinon": "^15.2.0",
"sync-request": "6.1.0",
"uuid": "^3.0.1"
},
Expand Down