1.1.0 • Published 23 days ago

jsmagicmock v1.1.0

Weekly downloads
2
License
MIT
Repository
github
Last release
23 days ago

JsMagicMock

A JavaScript implementation of Python's MagicMock. No dependencies, no build system, framework agnostic.

About

Every MagicMock is:

  • Callable
  • Infinitely nestable
  • Indexable
  • Object-like (key-values)
  • configurable

API

MagicMock is, at its core, a factory that returns a callable.

test('should instantiate', () => {
    expect(() => MagicMock()).not.toThrow();
});

Every MagicMock is an object. By default, all keys will return another MagicMock (thus, truthy).

test('should return an entity for any arbitrary named key', () => {
    const mock = MagicMock();

    expect(mock.whatever).not.toBeUndefined();
});

If you want a specific key to contain a specific value, you can define it in the factory call when your mock is constructed.

test('should accept object properties via the constructor', () => {
    const mock = MagicMock({
        something: 'yep'
    });

    expect(mock.something).toBe('yep');
});

This same principle works for deeply nested objects as well.

test('should accept deeply nested objects via the constructor', () => {
    const mock = MagicMock({
        something: {
            somethingElse: {
                onceMore: 'blarg'
            }
        }
    });

    expect(mock.something.somethingElse.onceMore).toBe('blarg');
});

Values do not have to be assigned at the factory. They can also be directly assigned at any time. And since every undefined key returns a MagicMock, you can keep going. Forever (or until you're out of memory)

test('should allow assignment of arbitrarily named/dynamic keys', () => {
    const mock = MagicMock();

    mock.something.what.yes.i.made.all.this.up = 'something';

    expect(mock.something.what.yes.i.made.all.this.up).toBe('something');
});

All object keys are also callable. You can treat any arbitrary key as if it were a function.

test('should allow invocation of arbitrarily assigned keys', () => {
    const mock = MagicMock();

    expect(() => {
        mock.something.what.yes.i.made.all.this.up();
        mock.__blarg_.yes.sure.whynot();
    }).not.toThrow();
});

The in keywork functions as you would expect.

test('"in" keyword with arbitrary keys', () => {
    const mock = MagicMock();

    expect('something' in mock).toEqual(true);
});
test('"in" keyword with deeply nested arbitrary keys', () => {
    const mock = MagicMock();

    expect('something' in mock.whatever.something.else).toEqual(true);
});

All of these arbitrary, magic keys are also numerically indexable like an array. This works on deeply nested keys as well.

test('indexed properties', () => {
    const mock = MagicMock(['a', 'b']);

    expect(mock[0]).toEqual('a');
    expect(mock[1]).toEqual('b');
});

test('deeply nested indexed properties', () => {
    const mock = MagicMock({
        something: ['a', 'b']
    });

    expect(mock.something[0]).toEqual('a');
    expect(mock.something[1]).toEqual('b');
});

They do not have to be defined as an array beforehand. Numeric keys will magically exist when you reference them.

test('can reference indexed properties without defining an array', () => {
    const mock = MagicMock();

    expect(mock.something.else[0]).not.toBeUndefined();
});

And just like other keys, they're callable like functions.

test('can invoke indexed propertes', () => {
    const mock = MagicMock([]);

    mock[0]('c', 'd');

    expect(mock[0].mock.calls[0].arguments).toEqual(
        ['c', 'd']
    );
});

test('can invoke deeply nested indexed propertes', () => {
    const mock = MagicMock([]);

    mock[0].whatever[2].yes('e', 'f');

    expect(mock[0].whatever[2].yes.mock.calls[0].arguments).toEqual(
        ['e', 'f']
    );
});

But what if you need a key to be purposefully absent? Any deleted keys will remain deleted.

describe('deleted keys', () => {

    test('purposefully deleted keys should remain deleted', () => {
        const mock = MagicMock();

        delete mock.whatever;

        expect(mock.whatever).toBeUndefined();
    });

    test('purposefully deleted deeply nested keys should remain deleted', () => {
        const mock = MagicMock();

        delete mock.something.i.made.up.whatever;

        expect(mock.something.i.made.up).not.toBeUndefined();
        expect(mock.something.i.made.up.whatever).toBeUndefined();
    });

    test('reassigning deleted keys works as expected', () => {
        const mock = MagicMock();

        delete mock.something.i.made.up;

        expect(mock.something.i.made.up).toBeUndefined();

        mock.something.i.made.up = 'i am back';

        expect(mock.something.i.made.up).toEqual('i am back');
    });
});

Mock API

There is a special meta object that contains an API for each individual mock.

Here you will find a way to set specific return values if you need, as well as meta information about whether the mock has been called.

describe('setting return values', () => {

    test('should return value specified via returnValue', () => {
        const mock = MagicMock();

        mock.mock.returnValue('asdf')

        expect(mock()).toEqual('asdf');
    });

    test('should return the exact entity specified via returnValue', () => {
        const mock = MagicMock();
        const testReturnValue = Symbol('test-symbol');

        mock.mock.returnValue(testReturnValue)

        expect(mock()).toEqual(testReturnValue);
    });

    test('deeply nested entities should respect returnValue', () => {
        const mock = MagicMock();

        mock.i.made.this.up.mock.returnValue('asdf')

        expect(mock.i.made.this.up()).toEqual('asdf');
    });
});

describe('with no calls', () => {

    test('should have an empty calls array', () => {
        const mock = MagicMock();

        expect(mock.mock.calls).toEqual([]);
    });

    test('should have a property `called` that is false', () => {
        const mock = MagicMock();

        expect(mock.mock.called).toBe(false);
    });

});

describe('with calls', () => {

    test('number of entry in calls should match number of function calls', () => {
        const mock = MagicMock();

        mock();
        mock();
        mock();

        expect(mock.mock.calls.length).toBe(3);
    });

    test('should have a property `called` that is true', () => {
        const mock = MagicMock();

        mock();

        expect(mock.mock.called).toBe(true);
    });

    test('each call arguments should be available', () => {
        const mock = MagicMock();

        mock.something(1, 'a');
        mock.something(2, 'b');

        expect(mock.something.mock.calls[0].arguments)
            .toEqual([1, 'a']);
        expect(mock.something.mock.calls[1].arguments)
            .toEqual([2, 'b']);
        expect(mock.something.mock.calls[2])
            .toBeUndefined();
    });

    test('calls should remain scoped to their object path', () => {
        const mock = MagicMock();

        mock.something(1, 'a');
        mock.something.else(2, 'b');

        expect(mock.mock.called).toBe(false);
        expect(mock.mock.calls.length).toBe(0);
        expect(mock.something.mock.called).toBe(true);
        expect(mock.something.mock.calls.length).toBe(1);
        expect(mock.something.mock.calls[0].arguments)
            .toEqual([1, 'a']);
        expect(mock.something.else.mock.called).toBe(true);
        expect(mock.something.else.mock.calls.length).toBe(1);
        expect(mock.something.else.mock.calls[0].arguments)
            .toEqual([2, 'b']);
    });
});

If you don't like where this key exists, you can name it anything you want by configuring it when your mock is constructed.

test('should return value specified via returnValue', () => {
    const mock = MagicMock(undefined, { metaKey: '___meta' });

    mock.___meta.returnValue('asdf')

    expect(mock()).toEqual('asdf');
});
test('deeply nested entities should respect returnValue', () => {
    const mock = MagicMock(undefined, { metaKey: '___meta' });

    mock.i.made.this.up.___meta.returnValue('asdf')

    expect(mock.i.made.this.up()).toEqual('asdf');
});
test('should have an empty calls array', () => {
    const mock = MagicMock(undefined, { metaKey: '___meta' });

    expect(mock.___meta.calls).toEqual([]);
});
test('number of entry in calls should match number of function calls', () => {
    const mock = MagicMock(undefined, { metaKey: '___meta' });

    mock();
    mock();
    mock();

    expect(mock.___meta.calls.length).toBe(3);
});
1.1.0

23 days ago

1.0.0

23 days ago

0.0.1

4 years ago