Spying on Functions with jest.spyOn()

In Jest and Vitest, spyOn is one of the most useful advanced features.

(Advanced feature, but very essential to writing tests every day).

It lets you track how many times a function was called, what it was called with, and you can (optionally) replace the implementation of a function.

Basic Example of using spyOn to check if a function was called.

const userService = {
  getUser: id => `User ${id}`,
};
const spy = jest.spyOn(
  userService,
  'getUser'
);
// or for vitest:
// vi.spyOn(userService, 'getUser')

userService.getUser('123');
expect(spy).toHaveBeenCalledWith('123');

(Note: jest.spyOn / vi.spyOn still call the original implementation by default; they only replace it if you set a mock return value or implementation. See below.)

You use spyOn on an object that contains a function.

If you do not add an implementation or a return value, then it is literally just a spy (it logs the calls, but doesn't change the real implementation)

It is more common to use spyOn along with a mock return value, which you can see below.

Note: You can import * as SomeImport from 'whatever', and then call vi.spyOn(SomeImport, 'fnToSpyOn') or jest.spyOn(SomeImport, 'fnToSpyOn') - examples below.

Simple example - track if a fn was called

class UserService {
  getUser(id) {
    return `User ${id}`;
  }
}

test('example of spyOn - check if a fn was called', () => {
  const userService = new UserService();

  // (or jest.spyOn(...)
  const getUserSpy = vi.spyOn(
    userService,
    'getUser'
  );

  const result =
    userService.getUser('123');
  expect(result).toBe('User 123'); // << note: this is still calling real implementation

  expect(
    getUserSpy
  ).toHaveBeenCalledWith('123');
});

Combining the spying with a mock return value

As you saw in the previous lesson, jest.fn()/vi.fn() tracks the usage, and can change the return value.

The spyOn returns the same as jest.fn() / vi.fn(), so you can still set mockReturnValue() etc.

test('example of spyOn - check if a fn was called & set a mock response', () => {
  const userService = new UserService();

  const getUserSpy = vi
    .spyOn(userService, 'getUser')
    .mockReturnValue(
      'mock user response'
    );

  const result =
    userService.getUser('123');
  expect(result).toBe(
    'mock user response'
  ); // << using the mock return value this time

  expect(
    getUserSpy
  ).toHaveBeenCalledWith('123');
});

Restoring a mock

One of the very useful things about using spyOn is that you can restore the original implementation

class UserService {
  getUser(id) {
    return `Real user ${id}`;
  }
}

test('example of spyOn - check if a fn was called', () => {
  const userService = new UserService();

  const getUserSpy = vi.spyOn(
    userService,
    'getUser'
  );

  expect(
    userService.getUser('123')
  ).toBe('Real user 123');

  getUserSpy.mockReturnValue(
    'mock user'
  );
  expect(
    userService.getUser('123')
  ).toBe('mock user');

  expect(
    getUserSpy
  ).toHaveBeenCalledTimes(2);

  getUserSpy.mockRestore(); // puts the original implementation back

  expect(
    userService.getUser('123')
  ).toBe('Real user 123');
});

Spying on a module import

Sometimes you need to spy on functions from imported modules.

You can do this by importing the entire module with import * as syntax:

// userUtils.js
export function formatUserName(
  firstName,
  lastName
) {
  return `${firstName} ${lastName}`;
}

export function validateEmail(email) {
  return email.includes('@');
}

then in your test:

// userService.test.js
import * as UserUtils from './userUtils'; // << this is important

test('spying on module exports', () => {
  // or jest.spyOn(UserUtils, 'formatUserName');
  const formatSpy = vi.spyOn(
    UserUtils,
    'formatUserName'
  );
  formatSpy.mockReturnValue(
    'MOCKED NAME'
  );

  const result =
    UserUtils.formatUserName(
      'Bob',
      'Bobby'
    );

  expect(result).toBe('MOCKED NAME');
  expect(
    formatSpy
  ).toHaveBeenCalledWith(
    'Bob',
    'Bobby'
  );

  // Restore original implementation
  formatSpy.mockRestore();
  expect(
    UserUtils.formatUserName(
      'Bob',
      'Bobby'
    )
  ).toBe('Bob Bobby');
});

You can also spy on multiple functions from the same module:

test('spying on multiple module exports', () => {
  const formatSpy = vi.spyOn(
    UserUtils,
    'formatUserName'
  );
  // or jest.spyOn(...)
  const validateSpy = vi.spyOn(
    UserUtils,
    'validateEmail'
  );

  formatSpy.mockReturnValue(
    'Mock User'
  );
  validateSpy.mockReturnValue(true);

  expect(
    UserUtils.formatUserName('a', 'b')
  ).toBe('Mock User');
  expect(
    UserUtils.validateEmail('invalid')
  ).toBe(true);

  expect(
    formatSpy
  ).toHaveBeenCalledTimes(1);
  expect(
    validateSpy
  ).toHaveBeenCalledTimes(1);
});

Lesson Task

In the spy on a simple object tests you need to spyOn someObject's getAge method. You can use vi.spyOn(someObject, 'getAge'). Then you use .mockReturnValue(99) to set what value the function returns when called.

For the second test, we want to spy on an imported module. Most of this test is written so it passes, but you have to check it was called and set the mock response

Ready to try running tests for this lesson?