moq.ts

Moq library for Typescript inspired by c# moq

Usage no npm install needed!

<script type="module">
  import moqTs from 'https://cdn.skypack.dev/moq.ts';
</script>

README

Build Status NPM version:latest NPM version:next npm downloads Dependency Status Commitizen friendly semantic-release Renovate enabled npm bundle size (minified + gzip) License

moq.ts | Documentation

Moq for Typescript. Inspired by c# Moq library.

Install

npm install moq.ts --save-dev

Quick start

moq.ts as the original Moq library is intended to be simple to use, strongly typed (no magic strings!, and therefore full compiler-verified and refactoring-friendly) and minimalistic (while still fully functional!). Every each mock is an instance of Proxy object.

You can find a pretty full set of usages in the integration tests. Check out tests.integration folder.



Mocking functions of objects

instance-method.spec.ts

import { Mock, It, Times } from "moq.ts";

interface ITestObject {
   method(arg1: number, arg2: string): Date;
}

const values = ["a", "b", "c"];

const mock = new Mock<ITestObject>()
   .setup(instance => instance.method(1, values[0]))
   .returns(new Date(2016))

   .setup(instance => instance.method(It.Is(value => value === 2), values[1]))
   .callback(({args: [arg1, arg2]}) => new Date(2017 + arg1))

   .setup(instance => instance.method(3, It.Is(value => value === values[2])))
   .throws(new Error("Invoking method with 3 and c"));

const object = mock.object();
const actual = object.method(1, "a");

mock.verify(instance => instance.method(2, "a"), Times.Never());

Mocking reading properties

read-property.spec.ts

import { Mock, It, Times, GetPropertyExpression } from "moq.ts";

interface ITestObject {
    property1: number;
    property2: number;
    property3: number;
    property4: number;

    method(): void;
}

const property4Name = "property4";
const mock = new Mock<ITestObject>()
    .setup(instance => instance.property1)
    .returns(1)

    .setup(instance => It.Is((expression: GetPropertyExpression) => expression.name === "property2"))
    .returns(100)

    .setup(instance => instance.property3)
    .callback(() => 10 + 10)

    .setup(instance => instance[property4Name])
    .throws(new Error("property4 access"))

    //since a method is a property that holds a pointer to a function
    .setup(instance => instance.method)
    .returns(() => {
        console.log("The method was called")
    });

const object = mock.object();
object.method();

mock.verify(instance => instance.property1, Times.Never());

Mocking writing properties

The documentation on returned value from "set hook" on Proxy object

set-property.spec.ts

import { Mock, It, Times, SetPropertyExpression } from "moq.ts";

interface ITestObject {
    property: number | any;
}

const value = {field: new Date()};

const mock = new Mock<ITestObject>()
    .setup(instance => {
        instance.property = 1
    })
    //true - allows the write operation
    .returns(true as any)

    .setup(instance => It.Is((expression: SetPropertyExpression) => expression.name === "property" && expression.value === 2))
    //false - denies the write operation
    .returns(false)

    .setup(instance => {
        instance.property = It.Is(value => value === 3)
    })
    // allows the write operation
    .callback(() => true)

    .setup(instance => {
        instance.property = value
    })
    .throws(new Error("an object has been written into property"));


const object = mock.object();
object.property = 1;

mock.verify(instance => {
    instance.property = 1
}, Times.Once());

Mocking functions

mock-method.property.IntegrationTests.ts

import { Mock, It, Times } from "moq.ts";

interface ITestFunction {
    (arg: number | any): string;
}

const value = {field: new Date()};

const mock = new Mock<ITestFunction>()
    .setup(instance => instance(1))
    .returns("called with 1")

    .setup(instance => instance(2))
    .callback(({args: [argument]}) => argument === 2 ? "called with 2" : `called with ${argument}`)

    .setup(instance => instance(value))
    .throws(new Error("Argument is object with date"))

    .setup(instance => instance(It.Is(value => value === 4)))
    .returns("called with 4");

const method = mock.object();
const actual = method(1);

mock.verify(instance => instance(1), Times.Once());
mock.verify(instance => instance(It.Is(value => value === 1)), Times.Exactly(1));


Auto mocking

The library support auto mocking for deep members. Consider this case:


interface IChild {
    get(): string;
}

interface IRoot {
    child: IChild;
}

const value = "value";

const child = new Mock<IChild>()
    .setup(instance => instance.get())
    .returns(value)
    .object();

const root = new Mock<IRoot>()
    .setup(instance => instance.child)
    .returns(child)
    .object();

const actual = root.child.get();

expect(actual).toBe(value);

We have to create a child mock in order to set up "child" property of the root object. With auto mocking this case could be rewritten as following:


const root = new Mock<IRoot>()
    // The root mock automatically creates a mock for the child property
    .setup(instance => instance.child.get())
    .returns(value)
    .object();

const actual = root.child.get();

expect(actual).toBe(value);

Type Discovering

Despite the fact that Mock class is generic type where T parameter is stands for mocked type it works only at design time. At the runtime phase the type is not available and Moq library relays on other ways to discover the type information. It is required to implement correct behaviour of mocked object.

Consider this case:

class Prototype {
    method(): void {
        throw new Error("Not Implemented");
    }
}

const object = new Mock<Prototype>()
    .object();

const actual = object.method(); // throws TypeError: object.method is not a function

expect(actual).toBe(undefined);

It happens because at runtime the mock does not know that method is a part of the mocked type. So at the moment there are 3 ways how type could be discovered at runtime.

Target

It is possible to provide a target object instance when a new instance of mock is being created. It will fix typeof operator. The prototype of the target will be used for type discovering and fixing instanceof operator.

By default, a mock is instantiated as Function object, so at runtime the mock "knows" about Function inherited properties and methods.

class Prototype {
    method(): void {
        throw new Error("Not Implemented");
    }
}

const object = new Mock<Prototype>({target: new Prototype()})
    .object();

const actual = object.method();

expect(actual).toBe(undefined);
expect(typeof object).toBe("object");
expect(object instanceof Prototype).toBe(true);

Prototype

Another way to deal with type discovering is to provide a prototype object.

class Prototype {
    method(): void {
        throw new Error("Not Implemented");
    }
}

const object = new Mock<Prototype>()
    .prototypeof(Prototype.prototype)
    .object();

const actual = object.method();

expect(actual).toBe(undefined);
expect(typeof object).toBe("function");
expect(object instanceof Prototype).toBe(true);

Setup examination

In some cases Moq library can discover type information from provided setup information.

class Prototype {
    method(): number {
        throw new Error("Not Implemented");
    }
}

const object = new Mock<Prototype>()
    .setup(instance => instance.method()) // this would be used for type discovering
    .returns(2)
    .object();

const actual = object.method();

expect(actual).toBe(2);
expect(typeof object).toBe("function");

Mock behavior

A mocked object is a Proxy, that configured to track any read and write operations on properties. If you write a value to an arbitrary property the mocked object will keep it and you can read it later on. By default, the prototype of mocked object is Function.

Accessing to an unset property or a method will return undefined, or a pointer to a spy function if it exists on prototype; You can call this function, and it will be tracked.

The default behaviour has the lowest precedence. The latest setup has the highest precedence.

You can control mock behavior when accessing to a property without a corresponding setup.

const mock = new Mock<ITestObject>();
mock.setup(() => It.IsAny())
    .throws(new Error("setup is missed"));

Injector config

Internally the library is using an injector that implementation is based on Angular injector to create and configure every each Mock object that is created with its constructor.

new Mock() // <-- calls angular based injector internally to create all dependencies 

The library provides an extension point to change the way how mocks are configured internally. It is available through IMockOptions.injectorConfig that could be applied globally or per mock instance at the instancing phase.

import { EqualMatchingInjectorConfig, Mock } from "moq.ts";

// The global configuration that would be overridden with instance options 
Mock.options = {injectorConfig: new EqualMatchingInjectorConfig()};
// or per instance
new Mock({injectorConfig: new EqualMatchingInjectorConfig()})

Out of the box there are 2 available configurations that change the way how a mock compares expressions.

DefaultInjectorConfig

This is a default configuration that provides the standard mock behaviours.

EqualMatchingInjectorConfig

By default, all values are matched with Equality comparisons and sameness that is limited in matching objects. On the other hand developers are using so called "deep equal comparison" approach, where objects are matched by its properties and values. This configuration changes the way how expressions are matched and introduce deep equal comparison logic as well as an extension point for custom matchers.

import { Mock } from "moq.ts";

const mock = new Mock<(args: number[]) => number>()
    .setup(instance => instance([2, 1]))
    .returns(2);

const object = mock.object();

const actual = object([2, 1]);

// since the default comparisons logic sees [2, 1] and [2, 1] as different objects the provided setup would not work
expect(actual).toBe(undefined);

and compare with

import { EqualMatchingInjectorConfig, Mock } from "moq.ts";

const mock = new Mock<(args: number[]) => number>({injectorConfig: new EqualMatchingInjectorConfig()})
    .setup(instance => instance([2, 1]))
    .returns(2);

const object = mock.object();

const actual = object([2, 1]);

expect(actual).toBe(2);

Internally the equal comparision logic implemented as a collection of object matchers that implement IObjectMatcher interface.

Matchers with the most specific logic should come first in the collection and if they are not able to match the objects then more general matchers would be invoked.

The library comes with the following matchers:

  1. Custom matchers
  2. DateMatcher - matches Date objects
  3. MapMatcher - matches Map objects
  4. IteratorMatcher - matches objects that supports Iterator protocol
  5. POJOMatcher - as the last resort matches objects as POJO objects.

Here is an example of a custom matcher that matches Moment and Date objects.

import { EqualMatchingInjectorConfig, IObjectMatcher, Mock, OBJECT_MATCHERS } from "moq.ts";
import { isMoment, utc } from "moment";

class MomentDateMatcher implements IObjectMatcher {
    matched<T extends object>(left: T, right: T): boolean | undefined {
        if (left instanceof Date && isMoment(right)) {
            return left.valueOf() === right.valueOf();
        }
        return undefined;
    }
}

const moment = utc(1);
const injectorConfig = new EqualMatchingInjectorConfig([{
    provide: OBJECT_MATCHERS,
    useClass: MomentDateMatcher,
    multi: true,
    deps: []
}]);

const mock = new Mock<(args: any) => number>({injectorConfig})
    .setup(instance => instance(moment))
    .returns(2);

const object = mock.object();

const actual = object(new Date(1));

expect(actual).toBe(2);

The matching logic of EqualMatchingInjectorConfig supports It notation . So you can do a partial comparision.

import { EqualMatchingInjectorConfig, It, Mock } from "moq.ts";

const func = () => undefined;

const injectorConfig = new EqualMatchingInjectorConfig();
const mock = new Mock<(args: any) => number>({injectorConfig})
    .setup(instance => instance({func: It.IsAny()})) // <-- func property will be matched with It delegate
    .returns(2);

const object = mock.object();

const actual = object({func});

expect(actual).toBe(2);

Internal API

The moq.ts library is comprised of small units that follow SOLID principles. Some of those units are included in the public API. The others are part of the internal API.

All of the units are composed together by an IoC container and make the library run. The IoC container's config is part of the public API, and developers can use it to change the behavior of any aspect of the library. In order to do this, developers need access to all public and internal units as well.

The core units are public and are available directly from the moq.ts package. Changes in those units follow Semantic Versioning, while changes in the internal units do not follow Semantic Versioning and could produce all types of version increments.

import * from "moq.ts/internal";

Internal API access provides wide opportunities to customize the library behavior. However, the user code that is based on the internal API could easily be broken by a new release.

Mock prototype

If you need to make work instanceof operator or you need to deal with prototype of the mock object you can use prototypeof function of Mock class. Or you can use Object.getPrototypeOf or Object.setPrototypeOf functions on mocked object.

class TestObject implements ITestObject {

}

const mock = new Mock<ITestObject>()
    .prototypeof(TestObject.prototype)
    .object();

mock.object() instanceof TestObject;// true

Mimics

If you need to replicate behaviour of an existing object you can reflect mock's interactions on the object.

class Origin {
    public property = 0;

    public method(input: number): number {
        return input * this.property;
    }
}

const origin = new Origin();

const mock = new Mock<Origin>()
    .setup(() => It.IsAny())
    .mimics(origin);

const mocked = mock.object();
mocked.property = 3;
const actual = mocked.method(2);

expect(actual).toBe(6);
mock.verify(instance => instance.method(2));

typeof operator

Some operations are not possible to trap in order to keep the language consistent, one of them is typeof. The type of the proxy object will be the same as the proxy target. So at the moment the only available options is to provider target option as a create mock parameter.

class Origin {
}

const origin = new Origin();

const mock = new Mock<Origin>({target: new Origin()});

expect(typeof mock.object()).toBe(typeof new Origin());

in operator

The library supports in operator. More examples could be found here

const name = "arbitrary name";
const object = new Mock<{}>()
    .setup(instance => name in instance)
    .returns(true)
    .object();

expect(name in object).toBe(true);
interface ITestObject {
    property: string;

    method(): void;
}

class TestObject implements ITestObject {
    property: string;

    method(): void {
        return undefined;
    }
}

const object = new Mock<ITestObject>()
    .prototypeof(TestObject.prototype)
    .object();

// because "property" in new TestObject() === false
expect("property" in object).toBe(false);
expect("method" in object).toBe(true);
const mock = new Mock<{}>();
const object = mock.object();

const actual1 = "property" in object;
const actual2 = "method" in object;

mock.verify(instance => "property" in instance, Times.Once());
mock.verify(instance => "method" in instance, Times.Once());

new operator

The library supports new operator. More examples could be found here

In order for the new operation to be valid on the resulting Proxy object, the target used to initialize the proxy must itself have a [[Construct]] internal method (i.e. new target must be valid).

class TestObject {
    constructor(public readonly arg) {
    }
}
    it("Returns new object with callback", () => {
    const value = "value";
    const mock = new Mock<typeof TestObject>({target: TestObject})
        .setup(instance => new instance(value))
        .callback(({args: [name]}) => new TestObject(name));
    const object = mock.object();

    const actual = new object(value);
    expect(actual).toEqual(new TestObject(value));
    mock.verify(instance => new instance(value));
});

it("Returns new object with returns", () => {
    const value = "value";
    const expected = new TestObject(value);
    const mock = new Mock<typeof TestObject>({target: TestObject})
        .setup(instance => new instance(value))
        .returns(expected);
    const object = mock.object();

    const actual = new object(value);
    expect(actual).toBe(expected);
    mock.verify(instance => new instance(value));
});

MoqAPI symbol

In some scenarios it is necessary to get Moq API from mocked object. For this purposes the library provides a predefined symbol MoqAPI. Mocked objects in their turns exposes a symbol property to access to its Moq API.

Since this property makes sense only in context of the moq library and is not specific for mocked types it is not possible to define an interaction behaviour with Setup API.

The property is read only and trackabel, so it possible to use for verification.

const func = new Mock<() => void>()
    .object();

func[MoqAPI]
    .setup(instance => instance())
    .returns(12);

const actual = func();

expect(actual).toBe(12);

In operator does not sees this property until it is used in setups.

const object = new Mock<{}>()
    .object();

expect(MoqAPI in object).toBe(false);

BUT

const mock = new Mock<ITestObject>();
const object = mock
    .setup(instance => instance[MoqAPI])
    .returns(undefined)
    .object();

expect(MoqAPI in object).toBe(true);
expect(object[MoqAPI]).toBe(mock);

Sponsored by 2BIT GmbH