terse-mock v1.0.20
JS/TS Tests in a Couple of Lines
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):
npm install --save-dev terse-mock
yarn
yarn add --dev terse-mock
Introduction
terse-mock is intended to use as an addition to existing test frameworks such as Jest.
The module mainly contains functions for creating and managing mocks/stubs and getting statistics on mocks usage. The most useful of them are:
terse-mock is covered and tested with Jest so Jest tests will be used in examples below.
Typical usage
import { tmock } from 'terse-mock';
test('some test', () => {
// ARRANGE
const mock = tmock('name', [ // create mock
[m => m.f('some value').data.val, 7], // setup return values
]);
// ACT
const res = tunmock(sut(mock)); // pass mock to system under test, unmock result
// ASSERT
expect(res).toEqual(expectedRes); // check expectations
}
Unmocking is required to turn result into plain js type before checking expectations.
Features
- Deep automocking
- Easy setting mock values
- Stubs
- Creating functions that return different values per set of arguments
- Interface mocks
- Call history
- Automatic spies
- Using external mocks
- Module mocks
- Resetting mocks
- And more
Deep automocking
By default mocks allows to access properties and call functions at any nesting level without first initializing the return values.
Suppose we have a SUT:
function sut(data) {
const r1 = data.getSomething(true).doSomething();
const r2 = r1.property === 1 ? data.getSomethingElse('a') : null;
const r3 = data.getSomethingElse('b', true);
r3.value = 7;
return {
prop1: r1.property,
prop2: r2,
prop3: r3,
};
}
The shortest test could be like:
test('shortest test demo', () => {
// ARRANGE
const mock = tmock('data');
// ACT
const res = tunmock(sut(mock));
// ASSERT
expect(res).toEqual({
prop1: 'data.getSomething(true).doSomething().property',
prop2: null,
prop3: {
value: 7,
},
});
});
Test Suites: 1 passed, 1 total
Easy setting mock values
For the SUT defined above make mock return different values for property so we could test all code paths:
test.each([
[1, 'something else'],
[0, null],
])('should return something else in prop2 when property is 1 otherwise null', (property, expectedResult) => {
// ARRANGE
const mock = tmock([
[m => m.getSomething(true).doSomething().property, property],
[m => m.getSomethingElse('a'), 'something else'],
]);
// ACT
const res = tunmock(sut(mock));
// ASSERT
expect(res.prop2).toEqual(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.
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
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
The module keeps history of calling functions from the mock. The test below demonstrates how one can check call order and arguments passed to functions:
test('checking calls demo', () => {
// ARRANGE
const mock = tmock([
[m => m.f1(), 1],
]);
// ACT
mock.f1();
mock.prop.f2(mock.a.b.c, false);
mock.prop.f2({ b: 'bbb' });
// ASSERT
expect(tinfo().callLog).toEqual([ // log of all calls
'mock.f1()',
'mock.prop.f2(mock.a.b.c, false)',
'mock.prop.f2({...})',
]);
expect(tinfo(mock.prop.f2).calls[1][0]).toEqual({ // examine arguments of a particular call
b: 'bbb',
});
});
Automatic spies
The module automatically creates spies for functions from mock return values added by tmock
and tset
(except functions from return values of other functions). Calls to spied functions get to call log and can be analyzed with tinfo
.
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 external mocks
terse-mock can use external mocks to analyze calls to mocked functions. To do so one need to create adapter for external 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('can use external mocks to analyze calls to mocked functions', () => {
// ARRANGE
const mock = tmock({ externalMock: jestMock });
// ACT
mock.f(7);
// ASSERT
const unmockedMock = tunmock(mock);
const externalMockForF = tinfo(unmockedMock.f).externalMock;
expect(externalMockForF).toBeCalledTimes(1);
expect(externalMockForF).toBeCalledWith(7);
});
Also external mocks can be used as return values for terse-mock mocks:
test('can use external mock as return value', () => {
// ARRANGE
const jestFn = jest.fn();
const mock = tmock([{ f: jestFn }]);
// ACT
mock.f();
// ASSERT
expect(mock.f).toBeCalledTimes(1);
});
Module mocks
terse-mock mocks can be used as return values from Jest module factory for jest.mock()
Please note that the example below uses the alternative way of setting mock values, as it is well suited for such cases.
jest.mock('some-module', () => tmock('some-module', [{
someFunction: () => 'some value',
}]));
Another example with expectation on mocked module function calls:
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 setup outside tmock
and tset
are cleared out from mock while values setup by tmock
and tset
persist. Calling treset
with mock argument will also reset all nested mocks passed to tmock
and tset
as return values for this mock.
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',
},
});
});
And more
Some of minor features are listed below. See project tests for the rest of features and examples.
Alternative way of setting mock values
Besides setup tuples there is another way of passing mock return values: initialization object. This option is well suited for module mocks.
test('two ways of setting mock values', () => {
// 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
import { tmock, TM_ANY } from 'terse-mock';
const mock = tmock([[m => m.f(TM_ANY), 1]]);
one can write
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 return values of tmock
and tset
.
test('nested mocks demo', () => {
// ARRANGE
const mock = tmock([
[m => m.nestedMock, tmock([
[mm => mm.prop1, 1],
[mm => mm.prop2, 3],
])],
[m => m.prop, 'val'],
]);
mock.nestedMock.anotherProp = 5;
// ASSERT
expect(mock.nestedMock.prop1).toBe(1);
expect(mock.nestedMock.prop2).toBe(3);
expect(mock.nestedMock.anotherProp).toBe(5);
expect(tunmock(mock)).toEqual({
nestedMock: {
prop1: 1,
prop2: 3,
anotherProp: 5,
},
prop: 'val',
});
Call history display options
By default module machinery shorten string representation of mock touches - it collapses the contents of objects, arrays and long mocks in called functions arguments. If you need to see the contents of objects and arrays, you can use the simplifiedOutput
option, which can be set both globally and for a specific mock.
test('simplified output enabled/disabled demo', () => {
// ARRANGE
const mockSimplified = tmock({simplifiedOutput: true });
const mock = tmock({ simplifiedOutput: false });
// ACT
const simplifiedResult = mockSimplified.f(
1,
tmock().f('long enough string to make mock shorten in output'),
{ a: 1 },
[1, 2, 3]
);
const result = mock.f(
1,
tmock().f('long enough string to make mock shorten in output'),
{ a: 1 },
[1, 2, 3],
);
// ASSERT
expect(tunmock(simplifiedResult)).toBe(
'mock.f(1, <...>, {...}, [...])');
expect(tunmock(result)).toBe(
`mock.f(1, mock.f('long enough string to make mock shorten in output'), {a: 1}, [1, 2, 3])`);
});
Global module options
tglobalopt
allows to customise global module settings e.g. set default name for mocks or turn simplified output on/of
test('global module options', () => {
// ARRANGE
tglobalopt({
defaultMockName: 'newName',
})
const mock = tmock();
// ACT
const res = tunmock(mock.a);
// ASSERT
expect(res).toBe('newName.a');
});
11 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago