(Updated: )
/ #jest #testing #javascript 

Jest .fn() and .spyOn() spy/stub/mock assertion reference

Curious about Advanced Jest Testing Features?

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.

Get "The Jest Handbook" (100 pages)

I want this

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

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:

More foundational reading for Mock Functions and spies in Jest:

unsplash-logoBernard Hermant

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Curious about Advanced Jest Testing Features?

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library. Get "The Jest Handbook" (100 pages)