Mocking named TypeScript imports during tests

How to stub named imports during unit tests

Note: you can find the companion source code in bahmutov/mock-ts-imports repository.

Imagine we have the following 2 TypeScript files.

math.ts
1
2
export const add = (a, b) => a + b
export const sub = (a, b) => a - b
user.ts
1
2
import { add } from './math'
export const compute = (a, b) => add(a, b)

Both files use named imports and exports which causes problems trying to stub them from the tests.

Testing direct named import

Let's write unit test to confirm the function add works. I will use Ava test runner. To directly load TS spec files (and source code), I will use ts-node and ava-ts.

1
2
3
4
5
$ npm i -D ava ava-ts typescript ts-node
+ [email protected]
+ [email protected]
+ [email protected]
+ [email protected]

Our first test

math-spec.ts
1
2
3
4
5
6
7
import test from 'ava'
import {add} from './math'

test('add', t => {
// testing the original function
t.deepEqual(add(2, 3), 5)
})

It passes

1
2
3
$ npx ava-ts math-spec.ts

1 passed

Testing transient named import

The module math.ts exports add that module user.ts calls during compute execution. Can we write a test for user.ts that stubs this indirect math.ts add export? Can we write a test where compute calls real add, and another test calls stubbed add?

user-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// "compute" imports "add" from "./math" using named import
import test from 'ava'
import { compute } from './user'

test('real add', t => {
// no stubbing
t.deepEqual(compute(2, 3), 5)
})

test('stubbed add', t => {
// somehow stub "add" from "./math.ts" to return known value like 100
stubSomehow('./math', 'add').returns(100)
t.deepEqual(compute(2, 3), 100)
})

Yes - by using a nice utility ts-mock-imports

1
2
3
$ npm i ts-mock-imports sinon
+ [email protected]
+ [email protected]

I am installing ts-mock-imports and its peer dependency Sinon.

Let's mock named imports, even if they are loaded indirectly.

user-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// "compute" imports "add" from "./math" using named import
import test from 'ava'
import { compute } from './user'
import { ImportMock } from 'ts-mock-imports'
// to mock "./math add" export need to import entire module
import * as math from './math'

test('real add', t => {
// no stubbing
t.deepEqual(compute(2, 3), 5)
})

test('stubbed add', t => {
// somehow stub "add" from "./math.ts" to return known value like 100
ImportMock.mockFunction(math, 'add', 100)
t.deepEqual(compute(2, 3), 100)
})

Note that we had to import ./math as math object to be able to mock a named import add.

1
2
3
$ npx ava-ts user-spec.ts

2 passed

Under the hood, the mockFunction uses Sinon stubs.

Restore mocks

Once mocked, the function math.add will stay mocked until restored.

user-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
test('real add', t => {
// no stubbing
t.deepEqual(compute(2, 3), 5)
})

test('stubbed add', t => {
// somehow stub "add" from "./math.ts" to return known value like 100
ImportMock.mockFunction(math, 'add', 100)
t.deepEqual(compute(2, 3), 100)
})

test('add stays stubbed', t => {
t.deepEqual(compute(-1, 7), 100)
})
1
2
3
$ npx ava-ts user-spec.ts

3 passed

I strongly recommend each test starts by restoring any mocked functions.

user-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeEach(ImportMock.restore)

test('real add', t => {
// no stubbing
t.deepEqual(compute(2, 3), 5)
})

test('stubbed add', t => {
// somehow stub "add" from "./math.ts" to return known value like 100
ImportMock.mockFunction(math, 'add', 100)
t.deepEqual(compute(2, 3), 100)
})

test('add stays stubbed', t => {
t.deepEqual(compute(-1, 7), 100)
})

The third test will correctly fail, because the mock add no longer returns 100.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ npx ava-ts user-spec.ts

2 passed
1 failed

add stays stubbed

/Users/gleb/git/mock-ts-imports/user-spec.ts:22

21: test('add stays stubbed', t => {
22: t.deepEqual(compute(-1, 7), 100)
23: })

Difference:

- 6
+ 100

You can also change and restore individual mock

1
2
3
4
5
6
7
8
test('stub and restore', t => {
const stub = ImportMock.mockFunction(math, 'add', 100)
t.deepEqual(compute(2, 3), 100)
stub.returns(42)
t.deepEqual(compute(2, 3), 42)
stub.restore()
t.deepEqual(compute(2, 3), 5)
})

More info