1.0.20 • Published 11 months ago

terse-mock v1.0.20

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

JS/TS Tests in a Couple of Lines

License: MIT coverage

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

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

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');
});
1.0.20

11 months ago

1.0.19

12 months ago

1.0.18

12 months ago

1.0.17

12 months ago

1.0.16

12 months ago

1.0.15

12 months ago

1.0.14

12 months ago

1.0.13

12 months ago

1.0.12

12 months ago

1.0.11

12 months ago

1.0.10

12 months ago

1.0.9

12 months ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago