2.2 Mock/Spy Assertion Reference
Where other JavaScript testing libraries would lean on a specific stub/spy library like Sinon - Standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework., Jest comes with stubs, mocks and spies out of the box.
We’ll tackle instantiatiating stubs, mocks and spies as well as which assertions can be done over them.
Assertions for a spy/mock/stub beyond Jest
The core assertions we tend to use for spies and stubs are used to answer the following questions:
- was the stub/spy called?
- was the stub/spy called the right amount of times?
- was the stub/spy called with the right arguments/parameters?
Assert a stub/spy has been called
In Jest, stubs are instantiated with jest.fn()
and they’re used with expect(stub).<assertionName>
.
Jest spies are instantiated using jest.spyOn(obj, 'functionName')
. Note: you can’t spy something that doesn’t exist on the object.
jest.toBeCalled()
and jest.toHaveBeenCalled()
are aliases of each other.
expect(stubOrSpy).toBeCalled()
passes if the stub/spy is called one or more times. expect(stubOrSpy).toBeCalled()
fails if the stub/spy is called zero times (ie. not called).
const myObject = {
doSomething() {
console.log('does something');
}
};
test('stub .toBeCalled()', () => {
const stub = jest.fn();
stub();
expect(stub).toBeCalled();
});
test('spyOn .toBeCalled()', () => {
const somethingSpy = jest.spyOn(myObject, 'doSomething');
myObject.doSomething();
expect(somethingSpy).toBeCalled();
});
In the same way expect(stubOrSpy).toHaveBeenCalled()
passes if the stub/spy is called one or more times. expect(stubOrSpy).toHaveBeenCalled()
fails if the stub/spy is called zero times (ie. not called).
const myObject = {
doSomething() {
console.log('does something');
}
};
test('stub .toHaveBeenCalled()', () => {
const stub = jest.fn();
stub();
expect(stub).toHaveBeenCalled();
});
test('spyOn .toHaveBeenCalled()', () => {
const somethingSpy = jest.spyOn(myObject, 'doSomething');
myObject.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
Replace a spied-on function’s implementation
For the spy example, note that the spy doesn’t replace the implementation of doSomething
, as we can see from the console output:
npx jest src/02.02-to-be-called.test.js src/02.02-to-have-been-called.test.js
PASS src/02.02-to-be-called.test.js
● Console
console.log src/02.02-to-be-called.test.js:3
does something
PASS src/02.02-to-have-been-called.test.js
● Console
console.log src/02.02-to-have-been-called.test.js:3
does something
Test Suites: 2 passed, 2 total
Tests: 4 passed, 4 total
In order to replace the spy’s implementation, we can use the stub/spy .mockImplementation()
or any of the mockReturnValue
/mockResolvedValue
functions.
const myObject = {
doSomething() {
console.log('does something');
}
};
test('spyOn().mockImplementation()', () => {
const somethingSpy = jest.spyOn(myObject, 'doSomething').mockImplementation();
myObject.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
test('spyOn().mockReturnValue()', () => {
const somethingSpy = jest.spyOn(myObject, 'doSomething').mockReturnValue();
myObject.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
The output for this suite is the following, as you can see, no console.log
s.
npx jest src/02.02-spy-mock-implementation.test.js
PASS src/02.02-spy-mock-implementation.test.js
✓ spyOn().mockImplementation() (4ms)
✓ spyOn().mockReturnValue() (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Assert that a stub/spy has not been called
Jest expect
has a chainable .not
assertion which negates any following assertion. This is true for stub/spy assertions like .toBeCalled()
, .toHaveBeenCalled()
.
The usual case is to check something is not called at all. However, the toHaveBeenCalledWith
and toHaveBeenCalledTimes
functions also support negation with expect().not
.
const myObject = {
doSomething() {
console.log('does something');
}
};
test('jest.fn().not.toBeCalled()/toHaveBeenCalled()', () => {
const stub = jest.fn();
expect(stub).not.toBeCalled();
expect(stub).not.toHaveBeenCalled();
});
test('jest.spyOn().not.toBeCalled()/toHaveBeenCalled()', () => {
const somethingSpy = jest.spyOn(myObject, 'doSomething').mockReturnValue();
expect(somethingSpy).not.toBeCalled();
expect(somethingSpy).not.toHaveBeenCalled();
});
The output for this would be as follows:
npx jest src/02.02-not-to-be-have-been-called.test.js
PASS src/02.02-not-to-be-have-been-called.test.js
✓ jest.fn().not.toBeCalled()/toHaveBeenCalled() (4ms)
✓ jest.spyOn().not.toBeCalled()/toHaveBeenCalled() (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Assert on a stub/spy call count
In a lot of situation it’s not enough to know that a function (stub/spy) has been called.
It’s important to make sure it’s been called a certain number of times. For example an increment
function being called once vs twice is very different.
let count = 0;
const counter = {
increment() {
count += 1;
},
getCount() {
return count;
}
};
const app = counter => {
counter.increment();
};
test('app() with mock counter .toHaveBeenCalledTimes(1)', () => {
const mockCounter = {
increment: jest.fn()
};
app(mockCounter);
expect(mockCounter.increment).toHaveBeenCalledTimes(1);
});
The output when run is as follows:
npx jest src/02.02-called-times.test.js
PASS src/02.02-called-times.test.js
✓ app() with mock counter .toHaveBeenCalledTimes(1) (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Spies vs stubs and mocks
// src/02.02-called-times.test.js
// other tests
test('app() with jest.spyOn(counter) .toHaveBeenCalledTimes(1)', () => {
const incrementSpy = jest.spyOn(counter, 'increment');
app(counter);
expect(incrementSpy).toHaveBeenCalledTimes(1);
});
The main difference is that the mockCounter
version wouldn’t allow the counter to increment.
So for example with the spyOn(counter)
approach, we can assert that counter.increment
is called but also getCount()
and assert on that.
test('app() with jest.spyOn(counter) .toHaveBeenCalledTimes(1)', () => {
// existing test setup/assertion code
expect(counter.getCount()).toEqual(1);
});
That’s the difference, in principle you shouldn’t either test the behaviour, in this case, that the counter has been incremented, or the internals, in this case, that the increment
function was called.
The output shows as still passing:
npx jest src/02.02-called-times.test.js
PASS src/02.02-called-times.test.js
✓ app() with mock counter .toHaveBeenCalledTimes(1) (3ms)
✓ app() with jest.spyOn(counter) .toHaveBeenCalledTimes(1) (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Assert on parameter or arguments of stub call(s)
Given the following application code which has a counter to which we can add arbitrary values, we’ll inject the counter into another function and assert on the counter.add
calls.
let state = 0;
const counter = {
add(value) {
state += value;
},
getCount() {
return state;
}
};
Given a singleAdd
function which calls counter.add(10)
, we want to be able to assert using jest.fn().toHaveBeenCalledWith()
and jest.spyOn().toHaveBeenCalledWith()
as follows.
const singleAdd = counter => {
counter.add(10);
};
test('singleAdd > jest.fn() toHaveBeenCalledWith() single call', () => {
const mockCounter = {
add: jest.fn()
};
singleAdd(mockCounter);
expect(mockCounter.add).toHaveBeenCalledWith(10);
});
test('singleAdd > jest.spyOn() toHaveBeenCalledWith() single call', () => {
const addSpy = jest.spyOn(counter, 'add');
singleAdd(counter);
expect(addSpy).toHaveBeenCalledWith(10);
});
Which gives the following output:
npx jest src/02.02-single-add.test.js
PASS src/02.02-single-add.test.js
✓ singleAdd > jest.fn() toHaveBeenCalledWith() single call (4ms)
✓ singleAdd > jest.spyOn() toHaveBeenCalledWith() single call (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Given a multipleAdd
function which calls counter.add(15)
and counter.add(20)
, we want to assert that both those calls are made.
const multipleAdd = counter => {
counter.add(15);
counter.add(20);
};
test('multipleAdd > jest.fn() toHaveBeenCalledWith() multiple calls', () => {
const mockCounter = {
add: jest.fn()
};
multipleAdd(mockCounter);
expect(mockCounter.add).toHaveBeenCalledWith(15);
expect(mockCounter.add).toHaveBeenCalledWith(20);
});
test('multipleAdd > jest.fn() toHaveBeenCalledWith() multiple calls', () => {
const addSpy = jest.spyOn(counter, 'add');
multipleAdd(counter);
expect(addSpy).toHaveBeenCalledWith(15);
expect(addSpy).toHaveBeenCalledWith(20);
});
Relevant output is as follows:
npx jest src/02.02-multiple-add.test.js
PASS src/02.02-multiple-add.test.js
✓ multipleAdd > jest.fn() toHaveBeenCalledWith() multiple calls (19ms)
✓ multipleAdd > jest.fn() toHaveBeenCalledWith() multiple calls (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
References
.toHaveBeenCalledWith
also supports all the expect.*
assertions that will be explained in section “4. Partial match assertions”.
Relevant Jest Documentation Links:
We’ve now seen how to assert over stubs and spies. We also did a deep dive into the differences between a spy and a stub in the context of testing, why one would be used over the other.
Next we’ll walk through an example of mocking a global object with the example of Date
mocking, which is quite useful in its own right.