terse-mock
Version:
Easy mock creation and automocking
472 lines (425 loc) • 14 kB
Markdown
# JS/TS Tests in a Couple of Lines
[](https://opensource.org/licenses/MIT)

The goal of this project is to make it easier to create mocks and stubs and to reduce the amount of code when writing tests in general.
## Install
npm ([terse-mock on npm](https://www.npmjs.com/package/terse-mock)):
```bash
npm install --save-dev terse-mock
```
yarn
```bash
yarn add --dev terse-mock
```
## Introduction
terse-mock is intended to use as an addition to existing test frameworks such as [Jest](https://jestjs.io/).
The module provides a number of functions:
<table style="line-height:0.7em">
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed;">tmock</td>
<td style="border: none;">creates mock</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">tstub</td>
<td>creates stub</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">tset</td>
<td>supplements mock or stub</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">treset</td>
<td>clears mock touches, mock calls and mock values set by sut; clears local options</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">tunmock</td>
<td>removes mock proxies from data</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">tinfo</td>
<td>provides information on mock usage</td>
</tr>
<tr style="border-bottom-style:hidden">
<td style="color:DarkRed">tglobalopt, tlocalopt</td>
<td>sets module options</td>
</tr>
</table>
terse-mock is tested with Jest so Jest tests are used in examples below.
### Typical usage
```javascript
import { tmock } from 'terse-mock';
test('some test', () => {
// ARRANGE
const mock = tmock([ // create mock
[m => m.prop1.prop2, true], // setup mock value
[m => m.f('some value').prop3, 7], // setup mock value
[m => m.f('some other value'), {}], // setup mock value
]);
// ACT
const res = sut(mock); // pass mock to system under test
// ASSERT
const unmockedRes = tunmock(res); // remove possible proxies (optional but strongly recommended)
expect(unmockedRes).toEqual(expectedResult); // check expectations
});
```
## Features
- [Deep automocking](#deep-automocking)
- [Setting mock values](#setting-mock-values)
- [Stubs](#stubs)
- [Creating functions that return different values per set of arguments](#creating-functions-that-return-different-values-per-set-of-arguments)
- [Interface mocks](#interface-mocks)
- [Call history](#call-history)
- [Automatic spies](#automatic-spies)
- [Using with 3rd party mocks](#using-with-3rd-party-mocks)
- [Module mocks](#module-mocks)
- [Resetting mocks](#resetting-mocks)
- [Other](#other)
- [Alternative way of setting mock values](#alternative-way-of-setting-mock-values)
- [Import all at once](#import-all-at-once)
- [Nested mocks](#nested-mocks)
- [Collapsing long arguments](#collapsing-long-arguments)
- [Module options](#module-options)
## Deep automocking
Normally one need to setup all mock values to get sut work.
Suppose we have a [SUT](https://en.wikipedia.org/wiki/System_under_test):
```javascript
function sut(obj) {
const r1 = obj.getSomething(true).doSomething();
const r2 = r1 ? obj.getSomethingElse('a').length : null;
const r3 = obj.getSomethingElse('b', true);
return {
prop1: r1,
prop2: r2,
prop3: r3,
};
}
```
The test for the whole return value could be like:
```javascript
test('should return expected result under certain conditions', () => {
// ARRANGE
const mock = tmock([
[m => m.getSomething(true).doSomething(), true],
[m => m.getSomethingElse('a').length, 1],
[m => m.getSomethingElse('b', true), 'something'],
]);
// ACT
const res = sut(mock);
// ASSERT
expect(tunmock(res)).toEqual({
prop1: true,
prop2: 1,
prop3: 'something',
});
});
```
Auto-mocking feature allows you to skip initialisation of mock values without breaking the sut.
Let's say you want to test only prop1 and don't care about prop2 and prop3.
Then the test could be like:
```javascript
test('should prop1 have some value under some conditions', () => {
// ARRANGE
const mock = tmock([
[m => m.getSomething(true).doSomething(), true],
]);
// ACT
const res = sut(mock);
// ASSERT
expect(tunmock(res).prop1).toBe(true);
});
```
Or even
```javascript
test('should prop1 have some value under some conditions, shortest test', () => {
// ARRANGE
const mock = tmock();
// ACT
const res = sut(mock);
// ASSERT
expect(tunmock(res).prop1).toBe('<mock>.getSomething(true).doSomething()');
});
```
```
Test Suites: 1 passed, 1 total
```
Here prop1 contains the path it got the value from.
## Setting mock values
For the SUT defined [above](#deep-automocking) make mock return different values for property so we could test all code paths:
```javascript
test.each([
['should prop2 have some value under some conditions', true, 10],
['should prop2 be null under some other conditions', false, null],
])('%s', (__, doSomethingResult, expectedResult) => {
// ARRANGE
const mock = tmock([
[m => m.getSomething(true).doSomething(), doSomethingResult],
[m => m.getSomethingElse('a').length, 10],
]);
// ACT
const res = sut(mock);
// ASSERT
expect(tunmock(res).prop2).toBe(expectedResult);
});
```
## Stubs
If one need neither automocking nor checking function calls then it worth using stubs rather then mocks. terse-mock stubs are plain js objects, fast and straightford.
```javascript
test('stub demo', () => {
// ARRANGE
const stub = tstub([
[s => s.a.aa, 0],
[s => s.f(), 'result'],
[s => s.b, { bb: 1 }],
]);
// ASSERT
expect(stub).toEqual({
a: { aa: 0 },
f: expect.any(Function),
b: { bb: 1 },
});
expect(stub.f()).toEqual('result');
});
```
## Creating functions that return different values per set of arguments
```javascript
test('function that return different values per set of arguments demo', () => {
// ARRANGE
const f = tstub([
[s => s(TM_ANY), 0],
[s => s(), 1],
[s => s('a'), 2],
[s => s('b'), 3],
[s => s('b', true), 4],
]);
// ASSERT
expect(f('something')).toEqual(0);
expect(f()).toEqual(1);
expect(f('a')).toEqual(2);
expect(f('b')).toEqual(3);
expect(f('b', true)).toEqual(4);
});
```
## Interface mocks
Generic form of `tmock`/`tstub` is available if one wants to use benefits like static type checking and code completion

## Call history
Module keeps history of function calls. The code below demonstrates how one can check call order and arguments passed to functions:
```javascript
test('check calls demo', () => {
// ARRANGE
const mock = tmock([
[m => m.f1(), 1],
]);
// ACT
mock.f1();
mock.prop.f2(1, false);
mock.prop.f2({ b: 'b' }).g(1);
// ASSERT
// All calls log
expect(tinfo().callLog).toEqual([
'<mock>.f1()',
'<mock>.prop.f2(1, false)',
`<mock>.prop.f2({b: 'b'})`,
`<mock>.prop.f2({b: 'b'}).g(1)`,
]);
// Examine arguments of a particular call
expect(tinfo(mock.prop.f2).calls[1][0]).toEqual({
b: 'b',
});
expect(tinfo(mock, m => m.prop.f2({ b: 'b' }).g).calls[0][0]).toBe(1);
});
```
This also can be useful for debugging purposes to examine all calls fo mocked functions in sut.
## Automatic spies
terse-mock automatically creates spies for js functions found in values passed to `tmock` and `tset`. Calls to this functions get to call log and can be analyzed with `tinfo`.
```javascript
test('automatic spies demo', () => {
// ARRANGE
const obj = {
nestedObj: {
f: function (n) { return n > 7 },
},
};
const mock = tmock([m => m.obj, obj]);
// ASSERT
expect(mock.obj.nestedObj.f(7)).toBe(false);
expect(mock.obj.nestedObj.f(8)).toBe(true);
expect(tinfo(mock).callLog).toEqual([
'<mock>.obj.nestedObj.f(7)',
'<mock>.obj.nestedObj.f(8)',
]);
});
```
## Using with 3rd party mocks
terse-mock can use 3rd party mocks to analyze calls to mocked functions. To do so one need to create adapter for 3rd party mock by implementing `IExternalMock` interface provided by the module and pass the adapter to `tmock`. The test demonstrates how to use Jest mocks for call analyzing:
```javascript
const jestMock: IExternalMock = {
create: () => jest.fn(),
};
test('using Jest function to analyze calls to mocked functions demo', () => {
// ARRANGE
tlocalopt({ externalMock: jestMock });
const mock = tmock();
// ACT
mock.f(7);
// ASSERT
const externalMockForF = tinfo(mock.f).externalMock;
expect(externalMockForF).toHaveBeenCalledTimes(1);
expect(externalMockForF).toHaveBeenCalledWith(7);
});
```
3rd party mocks can also be used as return values for terse-mock mocks:
```javascript
test('using Jest function as mock value demo', () => {
// ARRANGE
const jestFn = jest.fn();
const mock = tmock({ f: jestFn });
// ACT
mock.f();
// ASSERT
expect(mock.f).toHaveBeenCalledTimes(1);
});
```
## Module mocks
terse-mock mocks can be used as return values from [Jest module factory for jest.mock()](https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter)
Please note that the example below uses the [alternative way](#alternative-way-of-setting-mock-values) of setting mock values, as it is well suited for such cases.
```javascript
jest.mock('some-module', () => tmock('some-module', {
someFunction: () => 'some value',
}));
```
Another example with expectation on mocked module function calls:
```javascript
jest.mock('./module', () => tmock());
import { someFunction } from './module';
import { sut } from './sut-that-uses-module';
test('should call someFunction', () => {
// ACT
sut();
// ASSERT
expect(tinfo(someFunction).calls.length > 0).toBe(true);
});
```
## Resetting mocks
Mock or any of its part can be reset by `treset`. That means that all mock touches, mock calls and mock values that were setup outside `tmock` and `tset` (e.g in sut) are cleared out from mock while values setup by `tmock` and `tset` persist. Calling `treset` with *mock* argument will also reset all nested mocks.
```javascript
test('reset mocks demo', () => {
// ARRANGE
const mock = tmock([m => m.p.pp, 'val']);
// Oparate mock in sut.
mock.p = {}; // Replace value.
mock.a.f().c = true; // Add new value.
mock.b; // Touch.
expect(tunmock(mock)).toEqual({ // Unmock to observe all mock values at once
p: {},
a: {
f: expect.any(Function),
},
b: '<mock>.b',
});
// ACT
treset(mock);
// ASSERT
expect(tunmock(mock)).toEqual({
p: {
pp: 'val',
},
});
});
```
## Other
Some of minor features are listed below. Examine [terse-mock tests](https://github.com/ayartsev7/terse-mock/tree/main/tests) for the rest of features and examples.
### Alternative way of setting mock values
Besides setup tuples there is another way of setting mock values: initialization object. This option is well suited for [module mocks](#module-mocks).
```javascript
test('two ways of setting mock values demo', () => {
// ARRANGE
const stub = tstub([
{ // with object
a: 'value for a', // equivalent to tuple [s => s.a, 'value for a']
},
[s => s.b, 'value for b'], // with tuple
[s => s.c, 'value for c'], // with tuple
]);
// ASSERT
expect(stub).toEqual({
a: 'value for a',
b: 'value for b',
c: 'value for c',
});
});
```
### Import all at once
Module has default export with all module functions and constants.
Instead of
```javascript
import { tmock, TM_ANY } from 'terse-mock';
const mock = tmock([m => m.f(TM_ANY), 1]);
```
one can write
```javascript
import tm from 'terse-mock';
const mock = tm.mock([m => m.f(tm.ANY), 1]);
```
### Nested mocks
terse-mock mocks can be freely used as mock values in `tmock` and `tset`.
```javascript
test('nested mocks demo', () => {
// ARRANGE
const mock = tmock([
[m => m.nestedMock, tmock([ // nested mock
[mm => mm.prop1, 1],
])],
[m => m.prop, 'val'],
]);
mock.nestedMock.anotherProp = 5;
// ASSERT
expect(mock.nestedMock.prop1).toBe(1);
expect(mock.nestedMock.anotherProp).toBe(5);
expect(tunmock(mock)).toEqual({
nestedMock: {
prop1: 1,
anotherProp: 5,
},
prop: 'val',
});
});
```
### Collapsing long arguments
By default module machinery shorten string representation of mock touches - it collapses the contents of objects, arrays and long mocks in functions arguments. If you need to see the contents of collapsed data, you can use the `collapseLongValues` option. Use `collapseThreshold` option to set the string length threshold after which the data will be collapsed.
```javascript
test('collapsed output enabled/disabled demo', () => {
// ARRANGE
tlocalopt({
collapseLongValues: true,
collapseThreshold: 6,
});
// ACT
const result = tmock().f(
tmock('a').f(1), // length of 'a.f(1)' is 6
{ a: 1 }, // length of '{a: 1}' is 6
[1, 2], // length of '[1, 2]' is 6
tmock('aa').f(1), // length of 'aa.f(1)' > 6
{ a: 11 }, // length of '{a: 11}' > 6
[1, 2, 3], // length of '[1, 2, 3]' > 6
);
// ASSERT
expect(tunmock(result)).toBe('<mock>.f(a.f(1), {a: 1}, [1, 2], <...>, {...}, [...])');
});
```
### Module options
`tglobalopt` allows to customise module settings e.g. to set default name for mocks or turn automocking on/of.
`tlocalopt` allows to override module settings temporarly untill the `treset` is called.
```javascript
test('module options demo', () => {
// ARRANGE
tglobalopt({
defaultMockName: 'newDefaultMockName',
})
const mock = tmock();
// ACT
const res = tunmock(mock.a);
// ASSERT
expect(res).toBe('newDefaultMockName.a');
});
```