terse-mock
v2.0.0
Published
Easy mock creation and automocking
Downloads
303
Maintainers
Readme
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 provides a number of functions:
terse-mock is tested with Jest so Jest tests are used in examples below.
Typical usage
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
- Setting mock values
- Stubs
- Creating functions that return different values per set of arguments
- Interface mocks
- Call history
- Automatic spies
- Using with 3rd party mocks
- Module mocks
- Resetting mocks
- Other
Deep automocking
Normally one need to setup all mock values to get sut work.
Suppose we have a SUT:
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:
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:
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
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 make mock return different values for property so we could test all code paths:
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.
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
Module keeps history of function calls. The code below demonstrates how one can check call order and arguments passed to functions:
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
.
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:
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:
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()
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 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.
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 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.
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
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 values in tmock
and tset
.
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.
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.
test('module options demo', () => {
// ARRANGE
tglobalopt({
defaultMockName: 'newDefaultMockName',
})
const mock = tmock();
// ACT
const res = tunmock(mock.a);
// ASSERT
expect(res).toBe('newDefaultMockName.a');
});