autorewire | install autorewire | Cell 2 | Search

This code implements a basic JavaScript mocking framework called AutoMock that allows you to replace functions, objects, or properties with stubs during testing. It uses underscore for utility functions and tracks original and mock objects to prevent circular references.

Run example

npm run import -- "mock all properties and functions using rewire"

mock all properties and functions using rewire

var _ = require('underscore');
_.mixin(require('underscore.string').exports());

var path = require('path');
var util = require('util');

function simpleStubCreator(name) {
    return function stub() {
        // TODO: report arguments in a sane/safe way?
        console.log(_.sprintf('<stub for "%s()">', name));
    };
}


function AutoMock(parent) {
    this._parent = parent;
    this.setStubCreator();
}

AutoMock.prototype.setStubCreator = function (stubCreator) {
    this.defaultStubCreator = stubCreator || simpleStubCreator;
};

AutoMock.prototype._createMockingContext = function (params) {
    params = params || {};

    return {
        stubCreator: params.stubCreator || this.defaultStubCreator,

        passThru: params.passThru || [],

        // track originals and mocks in order to avoid circular references
        originals: [],
        mocks: []
    }
};

AutoMock.prototype.mockValue = function (orig, params) {
    params = params || {};
    var context = this._createMockingContext(params);
    return this._mockValue(params.name || '(none)', orig, context);
};

AutoMock.prototype._mockValue = function (name, orig, context) {
    var mock;
    var ignoredProperties;

    if (_.isObject(orig)) {
        // check for circular reference...
        var index = _.indexOf(context.originals, orig);
        if (index >= 0) {
            return context.mocks[index];
        }
        if (_.isFunction(orig)) {
            mock = context.stubCreator(name);
            ignoredProperties = functionIgnored;
        } else if (_.isArray(orig)) {
            mock = [];
            ignoredProperties = arrayIgnored;
        } else {
            mock = {};
        }

        context.originals.push(orig);
        context.mocks.push(mock);

        this._mockProperties(name, orig, mock, ignoredProperties, context);

        // The `prototype` property is excluded from property mocking because we
        // don't want to create a *new* prototype object.  Instead, we just want
        // to extend the mock's prototype with mocks of any prototype values of
        // the original.  (This is how we mock classes.)
        if (_.isFunction(orig) && !_.isUndefined(orig.prototype)) {
            this._mockProperties(name + '.prototype', orig.prototype, mock.prototype, prototypeIgnored, context);
        }
    } else {
        mock = orig;
    }

    return mock;
};

var functionIgnored = [
    'length',
    'name',
    'arguments',
    'caller',
    'prototype'
];

var prototypeIgnored = [
    'constructor'
];

var arrayIgnored = [
    'length'
];

AutoMock.prototype._mockProperties = function (name, orig, mock, ignoredProperties, context) {
    var keys = Object.getOwnPropertyNames(orig);

    var self = this;
    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];

        if (ignoredProperties && _.contains(ignoredProperties, key)) {
            continue;
        }

        // REVIEW: classes have a 'super_' property that points to the
        // superclass... we currently mock that, but don't create the
        // full class chain... if we run into problems with that, we'll
        // need better class/prototype support.

        var desc = Object.getOwnPropertyDescriptor(orig, key);
        var mockDesc;
        var fullName = _.sprintf('%s.%s', name, key);

        if (_.contains(context.passThru, fullName)) {
            mockDesc = desc;
        } else {
            mockDesc = {
                writeable: desc.writeable,
                enumerable: desc.enumerable,
                configurable: desc.configurable
            };

            if (desc.value) {
                mockDesc.value = self._mockValue(fullName, desc.value, context);
            }

            if (desc.get) {
                mockDesc.get = self._mockValue(_.sprintf('%s.%s', fullName, '__get__'), desc.get, context);
            }

            if (desc.set) {
                mockDesc.set = self._mockValue(_.sprintf('%s.%s', fullName, '__set__'), desc.set, context);
            }
        }

        Object.defineProperty(mock, key, mockDesc);
    }
};

// In order to "magically" normalize path references in terms of our "requirer",
// we have to be sure to re-require this module every time.  To do so, we remove
// the reference to this module from Node's require cache...
//delete require.cache[require.resolve(__filename)];

module.exports = new AutoMock(module.parent);

What the code could have been:

const _ = require('underscore');
_.mixin(require('underscore.string').exports());

const path = require('path');
const util = require('util');

// Create a stub creator function that logs a message to the console
const createStubCreator = (name) => {
    return function stub() {
        console.log(_.sprintf('<stub for "%s()">', name));
    };
};

// Create an AutoMock class that allows for mocking of objects and their properties
class AutoMock {
    constructor(parent) {
        this._parent = parent;
        this.setStubCreator();
    }

    // Set the stub creator function for the AutoMock instance
    setStubCreator(stubCreator = createStubCreator) {
        this.defaultStubCreator = stubCreator;
    }

    // Create a mocking context for the given parameters
    _createMockingContext(params = {}) {
        return {
            stubCreator: params.stubCreator || this.defaultStubCreator,

            passThru: params.passThru || [],

            originals: [],
            mocks: []
        };
    }

    // Create a mock value for the given original value and parameters
    mockValue(orig, params = {}) {
        const context = this._createMockingContext(params);
        return this._mockValue(params.name || '(none)', orig, context);
    }

    // Create a mock value for the given name, original value, and context
    _mockValue(name, orig, context) {
        let mock;
        let ignoredProperties;

        if (_.isObject(orig)) {
            // Check for circular references
            const index = _.indexOf(context.originals, orig);
            if (index >= 0) {
                return context.mocks[index];
            }

            if (_.isFunction(orig)) {
                mock = context.stubCreator(name);
                ignoredProperties = functionIgnored;
            } else if (_.isArray(orig)) {
                mock = [];
                ignoredProperties = arrayIgnored;
            } else {
                mock = {};
            }

            context.originals.push(orig);
            context.mocks.push(mock);

            this._mockProperties(name, orig, mock, ignoredProperties, context);

            // Mock the prototype property of the original function
            if (_.isFunction(orig) &&!_.isUndefined(orig.prototype)) {
                this._mockProperties(name + '.prototype', orig.prototype, mock.prototype, prototypeIgnored, context);
            }
        } else {
            mock = orig;
        }

        return mock;
    }

    // Mock the properties of the given original value and mock
    _mockProperties(name, orig, mock, ignoredProperties, context) {
        const keys = Object.getOwnPropertyNames(orig);

        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];

            if (ignoredProperties && _.contains(ignoredProperties, key)) {
                continue;
            }

            const desc = Object.getOwnPropertyDescriptor(orig, key);
            const mockDesc;
            const fullName = _.sprintf('%s.%s', name, key);

            if (_.contains(context.passThru, fullName)) {
                mockDesc = desc;
            } else {
                mockDesc = {
                    writable: desc.writable,
                    enumerable: desc.enumerable,
                    configurable: desc.configurable
                };

                if (desc.value) {
                    mockDesc.value = this._mockValue(fullName, desc.value, context);
                }

                if (desc.get) {
                    mockDesc.get = this._mockValue(_.sprintf('%s.%s', fullName, '__get__'), desc.get, context);
                }

                if (desc.set) {
                    mockDesc.set = this._mockValue(_.sprintf('%s.%s', fullName, '__set__'), desc.set, context);
                }
            }

            Object.defineProperty(mock, key, mockDesc);
        }
    }
}

const functionIgnored = [
    'length',
    'name',
    'arguments',
    'caller',
    'prototype'
];

const prototypeIgnored = [
    'constructor'
];

const arrayIgnored = [
    'length'
];

// Export the AutoMock instance
module.exports = new AutoMock(module.parent);

// TODO: report arguments in a sane/safe way?

// The code has been refactored to follow the Single Responsibility Principle (SRP)
// and the Don't Repeat Yourself (DRY) principle. The code has been made more modular
// and easier to maintain. The TODO comment has been kept for future reference.

This code defines a simple mocking framework called AutoMock for JavaScript.

Here's a breakdown:

1. Setup:

2. simpleStubCreator Function:

3. AutoMock Class:

In summary: This code implements a basic mocking framework that allows you to replace functions, objects, or properties with stubs during testing. It uses underscore for utility functions and tracks original and mock objects to prevent circular references.