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.logs.

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.

Jump to table of contents