Omnimock
Delicious mocks for TypeScript
Install / Use
/learn @hmil/OmnimockREADME
OmniMock - Mocks for TypeScript

OmniMock is a test mock library for TypeScript, with a focus on strong typing and ease of use.
Unlike other similar libraries, OmniMock was built from the ground up for TypeScript and makes no compromise.
Getting started
Check out the quick start guide to get up to speed in minutes. Do come back here if you want to learn why this library was built the way it was built and why it is a good long-term investment for your projects.
Requirements
OmniMock aims to bring the best possible mocking experience in TypeScript with no compromise. This comes at the cost of a few prerequisites:
- TypeScript 3.1 or above
- A runtime which supports the Proxy API (it can not be polyfilled, but any recent version of Node.js or mainstream browser will do)
Philosophy
A mock library attempts to fulfill multiple contradictory goals. Each author choses to solve the conflicts in a different way and this is why there are many mocking libraries out there. OmniMock uses a principled approach to solve these goals in a rational way. We believe that by basing all design decisions on this list, we create something that is not just nice to look at on github, but also increases productivity when used in the real world.
- Strong typing is just as necessary in test code as it is in production code. By using
anyall over the place, or by creating ad-hock objects as mocks, you lose the connection between your production code and your test code. Features like symbol renaming and usage lookup don't apply to your tests which makes it harder to understand and refactor your codebase. What's more, loose types mean your tests might get out of sync without you even noticing, and at some point they basically become dead weight. - Sound design. When something works, it always works. Some APIs such as that of substitute generate namespace and type conflicts. Then you have to use a workaround if you are unfortunate enough to use the same name as that of a method of the framework, and you need to turn off strict null checking. These exceptions are here because the author chose to favor fewer keystrokes over soundness. In OmniMock, the mock control and the mock object are two different objects with different types, which makes for a more robust API with no special cases. No need to disable strict type checking and no risk for your type to collide with that of OmniMock.
- Ease of use comes only third in the list. JavaScript is a very flexible language which lets you design APIs however you like them. Some authors get carried away and bundle as much "nice to haves" as they can without thinking about soundness or type safety.
That being said, one likes a nice API. That is why OmniMock offers powerful utilities such as automatic chaining for deeply nested properties, a single entry point to generate any kind of mock, wether it's a class instance, interface, function or object.
Helpers like
.resolveand.rejecthelp you reduce the boilerplate without losing any type safety.
Introduction
Why use a mocking library?
Unit tests need to be isolated. In order to achieve this, many guides show basic examples of what we can call "manual mocking".
const fakeCard: MtgCard = {
cost: 'UW',
color: 'white',
kind: 'sorcery',
art: 'John Avon',
play: () => ({} as any),
burry: () => ({} as any),
// ...
}
const gameState: MtgState = {
player1: {
getManaPool() {
return [ { color: 'W', qty: 1 } ];
},
// ...
},
player2: {
getManaPool() {
return [ ]; // Not important
},
// ...
},
clock: {
player: 1,
phase: 'precombat main'
}
}
const result = battlefield.castSpell(gameState, fakeCard);
expect(result.type).toBe('illegal move');
expect(result.reason).toBe('not enough mana');
The setup of the mocks is huge, it is hard to maintain, contains a lot of useless data, and juggles with types. IDE features like symbol lookup and renaming don't work in test code. The fake data may become inconsistent with the expected type and unit tests exercise situations which were already ruled out by the type system.
Developpers are incentivised to rely on actual implementations of the dependencies rather than mocks. This can quickly pile up to a mountain of tech debt and deter anyone from attempting any kind of refactoring on the main codebase.
Enters a mocking library.
The main feature a mocking library brings to the table is automatic mocks. What these allow you to do is focus on what is relevant to your specific test case, and ignore all of the rest. All of this while increasing type safety across your entire test suite.
Going back to the example above, we know that the battlefield class will only need the card's mana cost, player 1's mana reserve and the clock data. We can rewrite the test like this.
const fakeCard = mock<MtgCard>('fakeCard', {
cost: 'UW'
});
const gameState = mock<MtgState>('gameState', {
clock: {
player: 1,
phase: 'precombat main'
}
});
when(gameState.player1.getManaPool())
.return([ { color: 'W', qty: 1 } ])
.once();
const result = battlefield.castSpell(instance(gameState), instance(fakeCard));
expect(result.type).toBe('illegal move');
expect(result.reason).toBe('not enough mana');
You may argument that the above can easily be achieved by creating a manual mock of Partial<MtgCard> and Partial<MtgState>, and only populating the relevant field. Effectively, this would be very similar to what we just did above. However, there are two problems with this approach:
- You will have to force a cast from
Partial<T>toT. This is TypeScript telling you that you are doing something wrong... - ...The wrong thing being: your production code is not equipped to deal with undefined values from your
Partial. If it turns out the code uses some of the properties you thought it did not use, then it is possible that the undefined value will trickle down your system and cause an Error far from the original place the culprit value came from.
By contrast, if you forget to mock something, or if you make some changes to the code of battlefield, then OmniMock might throw an error like this: Error: Unexpected call to MtgCard.play(), with a stacktrace pointing to the exact location where the unexpected call occurred.
Features
<a name="types-of-mock"></a> Creating a mock
Use the mock() method to create a mock. Set expectations on the object returned by the mock() method, and pass the instance of that mock to your tested code.
// Use the mock to set expectations
const someServiceMock = mock(SomeService);
when(someServiceMock.doStuff()).return('hi').once();
// Pass the instance to the class you are testing
const someService = instance(myMock);
const testClass = new TestClass(someService);
instance() always returns a reference to the same object. The expectations you set on the mock always affect the instance, even after you've obtained a reference to it.
If you do not need to set expectations on the mock, you can use mockInstance as a shorthand for instance(mock()).
<a name="virtual-mock"></a> Virtual mocks
You can mock any object type or interface without providing an actual instance of it. This is called a virtual mock because it doesn't retain any type information at runtime.
Virtual mocks have some limitations. Learn more here.
// Mock an interface by passing it as a type argument to the mock function.
// Give it a name to help print nice error messages
const mockAssemblyService = mock<AssemblyService>('mockAssemblyService');
// Mock a class from its constructor. The name and type are infered automatically.
const mockAssemblyService = mock(AssemblyService);
<a name="backed-mock"></a> Backed mocks
Passing an actual object to the mock function creates a backed mock. Backed mocks support features such as .callThrough() and .useActual(), and work with code which uses property enumeration. See backed mock.
const realAssemblyService = {
assemble(parts: PartsList, blueprint: Blueprint) { /* ... */ }
version: 2
};
// realAssemblyService "backs" assemblyServiceMock
const assemblyServiceMock = mock('assemblyServiceMock', realAssemblyService);
// This works well with classes too
const assemblyServiceMock = mock('assemblyServiceMock', new AssemblyService());
By default, property access on a backed mock don't trigger an error (ie. the
