Skip to content

Quick start guide

Hadrien Milano edited this page Sep 9, 2019 · 3 revisions

Creating a mock

Using mock, when and instance

interface FoodPlant {
    process(ingredient: 'potato' | 'tomato'): number;
}

import { mock, instance, when } from 'omnimock';

// Step 1: Create a mock
const plantMock = mock<FoodPlant>('plantMock');

// Step 2: Define behaviors
when(plantMock.process('potato')).return(2);

// Step 3: Use your mock
const foodPlant = instance(plantMock);

foodPlant.process('potato') === 2; // true
foodPlant.process('tomato');       // Error: No behavior defined for <plantMock>.process('tomato')

This method works best for classes which belong to the C in MVC (services, controllers, ...). Check out mockInstance for mocking DTOs and other data objects.

Long call chains can be matched just as easily:

interface ProcessResult {
    getQuantityOf(ingredient: string): number;
}
interface FoodPlant {
    process(...ingredients: Array<string>): ProcessResult;
}

// ...

when(plantMock.process('potato', 'tomato').getQuantityOf('tomato')).return(4);

foodPlant.process('potato', 'tomato').getQuantityOf('tomato') === 4; // true

Argument matching

Use argument matchers to capture a large subset of possible calls at once:

import { anyOf } from 'omnimock';

when(plantMock.process(anyOf('potato', 'tomato'))).return(4);

foodPlant.process('tomato') === foodPlant.process('potato'); // true ( === 4)

See the full list of available matchers.

Calling a fake

Using a custom function is a powerful way to customize the behavior of a mock.

import { anyString } from 'omnimock';

when(plantMock.process(anyString())).call(i => i.charAt(0) === 'p' ? 4 : 0);

foodPlant.process('potato') // 4
foodPlant.process('tomato') // 0

The documentation shows all possible ways to define the result of a call.

Using mockInstance

mockInstance(...) is a shorthand for instance(mock(...)).

interface Vegetable {
    type: string;
    edible: boolean;
}

import { mockInstance } from 'omnimock';

const edibleMock = mockInstance<Vegetable>('edibleMock', { edible: true });

expect(edibleMock.edible).toBe(true);

This technique is convenient for working with DTOs and other simple data objects.

Nested objects can be mocked with nested calls.

interface Vitamin {
    letter: string;
}

interface Vegetable {
    // ...
    vitamins: Array<Vitamin>;
}
    
const ascorbic = mockInstance<Vegetable>('ascorbic', {
    edible: true,
    vitamins: [
        mockInstance<Vitamin>('vitamin', { letter: 'c' })
    ]
});

Tips & Tricks

Mock factory shorthand

If what you are mocking is an actual class, then you may pass that class as the first argument to mock or mockInstance.

Note: This does not work with abstract classes or interfaces.

// The regular usage: Specify the type and the name of the mock
const edibleMock = mockInstance<Vegetable>('edibleMock', { edible: true });

// Shorthand: the type and name of the mock are inferred.
const edibleMock = mockInstance(Vegetable, { edible: true });

// Works with `mock` too
const edibleMock = mock(Vegetable);

The nested example from the previous section can be rewritten like this:

const ascorbic = mockInstance(Vegetable, {
    edible: true,
    vitamins: [
        mockInstance(Vitamin, { letter: 'c' })
    ]
});

instanceof

In order for instanceof to work properly, you need to use the constructor shorthand syntax:

const edibleMock = mock<Vegetable>('edibleMock', { edible: true });
instance(edibleMock) instanceof Vegetable // false

const edibleMock = mock(Vegetable, { edible: true });
instance(edibleMock) instanceof Vegetable // true

Type casts

If you find yourself having to do a type cast, then most likely you are doing something wrong. Omnimock is designed to eliminate the need for casts.

const myVegetable: Vegetable = {
    edible: true
} as Partial<Vegetable> as Vegetable; // A bad type cast!

// Instead, do:
const myVegetable = mockInstance<Vegetable>('myVegetable', {
    edible: true
});

Why is the later better than the former? Omnimock's cast from Partial<T> to T is type-safe because it guarantees that no undefined behavior may occur. See Why use a mocking library for more details.

Generic functions

Type arguments of functions are lost when the function is mocked. This is due to a limitation in TypeScript's generics.

function myGeneric<T extends string>(t: T): T { return t; }
const dict = { hello: 'world' };
const mockGeneric = mock<typeof myGeneric>('myGeneric');

// Error: s should be of type 'hello' but it is instead of type 'string' 
when(mockGeneric('hello')).call(s => dict[s]);

// You need an explicit cast to fix this
when(mockGeneric('hello')).call(s => dict[s as 'hello']);

Keep this in mind when dealing with generic methods or functions.