Jest .fn() and .spyOn() spy/stub/mock 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.
This post looks at how to instantiate stubs, mocks and spies as well as which assertions can be done over them.
Table of Contents
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?
jest.toBeCalled()/.toHaveBeenCalled()
: 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 myObj = {
doSomething() {
console.log('does something');
}
};
test('stub .toBeCalled()', () => {
const stub = jest.fn();
stub();
expect(stub).toBeCalled();
});
test('spyOn .toBeCalled()', () => {
const somethingSpy = jest.spyOn(myObj, 'doSomething');
myObj.doSomething();
expect(somethingSpy).toBeCalled();
});
See Running the examples to get set up, then run:
npm test src/to-be-called.test.js
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 myObj = {
doSomething() {
console.log('does something');
}
};
test('stub .toHaveBeenCalled()', () => {
const stub = jest.fn();
stub();
expect(stub).toHaveBeenCalled();
});
test('spyOn .toHaveBeenCalled()', () => {
const somethingSpy = jest.spyOn(myObj, 'doSomething');
myObj.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
See Running the examples to get set up, then run:
npm test src/to-have-been-called.test.js
.spyOn().mockImplementation()
to 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:
jest
PASS src/to-be-called.test.js
● Console
console.log src/to-be-called.test.js:3
does something
PASS src/to-have-been-called.test.js
● Console
console.log src/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 myObj = {
doSomething() {
console.log('does something');
}
};
test('spyOn().mockImplementation()', () => {
const somethingSpy = jest.spyOn(myObj, 'doSomething').mockImplementation();
myObj.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
test('spyOn().mockReturnValue()', () => {
const somethingSpy = jest.spyOn(myObj, 'doSomething').mockReturnValue();
myObj.doSomething();
expect(somethingSpy).toHaveBeenCalled();
});
The output for this suite is the following, as you can see, no console.log
s.
jest src/spy-mock-implementation.test.js
PASS src/spy-mock-implementation.test.js
✓ spyOn().mockImplementation() (4ms)
✓ spyOn().mockReturnValue()
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
See Running the examples to get set up, then run:
npm test src/spy-mock-implementation.test.js
jest.not.toBeCalled()/.not.toHaveBeenCalled()
: asserting 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 myObj = {
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(myObj, 'doSomething').mockReturnValue();
expect(somethingSpy).not.toBeCalled();
expect(somethingSpy).not.toHaveBeenCalled();
});
See Running the examples to get set up, then run:
npm test src/not-to-be-have-been-called.test.js
jest.toHaveBeenCalledTimes()
: asserting 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);
});
test('app() with jest.spyOn(counter) .toHaveBeenCalledTimes(1)', () => {
const incrementSpy = jest.spyOn(counter, 'increment');
app(counter);
expect(incrementSpy).toHaveBeenCalledTimes(1);
});
See Running the examples to get set up, then run:
npm test src/to-have-been-called-times.test.js
Spies vs stubs and mocks
In the previous example, why would we use a complete mock vs a spy?
test('app() with mock counter .toHaveBeenCalledTimes(1)', () => {
const mockCounter = {
increment: jest.fn()
};
app(mockCounter);
expect(mockCounter.increment).toHaveBeenCalledTimes(1);
});
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.
jest.toHaveBeenCalledWith()
: asserting on parameter/arguments for 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.
See Running the examples to get set up, then run:
npm test src/to-have-been-called-with.test.js
let state = 0;
const counter = {
add(val) {
state += val;
},
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);
});
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);
});
Running the examples
Clone github.com/HugoDF/jest-spy-mock-stub-reference.
Run yarn install
or npm install
(if you’re using npm
replace instance of yarn
with npm run
in commands).
Further Reading
All the expect.*
constructs works with .toHaveBeenCalledWith
:
.objectContaining
and.arrayContaining
as seen in Jest Array/Object partial match with objectContaining and arrayContainingexpect.anything
as seen in Jest assert over single or specific argument/parameters with .toHaveBeenCalledWith and expect.anything()
More foundational reading for Mock Functions and spies in Jest:
Get The Jest Handbook (100 pages)
Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.
orJoin 1000s of developers learning about Enterprise-grade Node.js & JavaScript