Testing TypeScript with Jest

Installation and Jest setup

Refer to https://jestjs.io/docs/getting-started

Matchers

Full list of matchers: https://jestjs.io/docs/expect

Matching exact equality with toBe

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

Matching value equality with toEqual

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

Matching opposites with not

test('two plus two is four', () => {
  expect(2 + 2).not.toBe(22);
});

Matching truthiness, null, undefined and defined

  • toBeNull matches only null
  • toBeUndefined matches only undefined
  • toBeDefined is the opposite of toBeUndefined
  • toBeTruthy matches anything that an if statement treats as true
  • toBeFalsy matches anything that an if statement treats as false

Matching numbers

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

Matching floating point numbers

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

Matching strings using regular expression with toMatch

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

Matching arrays and iterables with toContain

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('the shopping list has milk on it', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

Matching exceptions with toThrow

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use a string that must be contained in the error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // Or you can match an exact error message using a regexp like below
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

Testing Promises

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

Make sure to return the Promise, otherwise the test will complete prematurely.

Testing async/await with .resolves and .rejects

test('the data is peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  await expect(fetchData()).rejects.toMatch('error');
});

Testing callbacks with done

test('the data is peanut butter', done => {
  function callback(error, data) {
    if (error) {
      done(error);
      return;
    }
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }

  fetchData(callback);
});

Inspecting a jest.fn() mock

jest.fn() is used to create a new, standalone mock function. It’s not tied to any specific object or method, and you can use it to replace any function in your test code. The mock property on a jest.fn() tracks calls, results, contexts, instances and lastCall:

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

Alternatively, there are convenient matchers:

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

Spying on implementation with jest.spyOn

jest.spyOn is primarily used to spy on existing functions or methods, tracking their calls and behavior without modifying their implementation.

const obj = {
  method: () => {}
};

const spy = jest.spyOn(obj, 'method');

// Now 'method' is a spy, you can check if it's been called, etc.
obj.method();
expect(spy).toHaveBeenCalled();

Mocking jest.fn() return values

// By default a mock returns undefined
const myMock = jest.fn();
console.log(myMock());

// Mock can be chained
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Mocking implementation of a function with jest.fn or the mockImplementation

Instead of just mocking the return values, we can mock the whole functionality of a function:

jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

We can also change implementation on a per-invocation basis:

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

Mocking functions that must return this keyword (to allow chaining):

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
  myMethod: jest.fn(function () {
    return this;
  }),
};

Adding a name with mockName

To be able to quickly identify the mock function reporting an error in your test output.

const myMockFn = jest
  .fn()
  .mockReturnValue('default')
  .mockImplementation(scalar => 42 + scalar)
  .mockName('add42');

Mocking a whole module with jest.mock

This code uses a third-party module axios to make a request.

import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

We want to mock the whole axios module, especially axios.get function to prevent making a real request. By specifying jest.mock('axios') jest automatically mocks the whole module, effectively replacing every method in axios module with a jest.fn() function, giving us the possibility to return custom mock-values.

import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});

Mocking a whole module with jest.mock and __mocks__ folder (Manual mocks)

https://jestjs.io/docs/manual-mocks

Let’s assume your code looks like that:

import user from './user';

class Employees {
  static all() {
    return user.getThemAll();
  }
}

export default Employees;

You want to mock the whole user module. First locate user.js, then create a folder __mocks__ on the same level, then add an empty user.js to __mocks__ folder and insert your mock code:

// __mocks__/user.js

// Define a mock implementation for the user module

const user = {
  getThemAll: jest.fn().mockReturnValue(['John', 'Jane', 'Doe']),
};

export default user;

And your test file

// employees.test.js

import Employees from './Employees'; // Import the module to be tested
import user from './user'; // Import the mocked user module

describe('Employees', () => {
  // Test suite for the Employees class
  
  // Test case for the static all() method
  describe('all', () => {
    it('returns an array of employees', () => {
      // Define a mock implementation for user.getThemAll()
      user.getThemAll.mockReturnValue(['John', 'Jane', 'Doe']);
      
      // Call the static all() method of Employees
      const employees = Employees.all();
      
      // Assertions
      expect(employees).toEqual(['John', 'Jane', 'Doe']); // Check if the return value is as expected
      expect(user.getThemAll).toHaveBeenCalled(); // Check if user.getThemAll() has been called
    });
  });
});

Mocking parts of a module with jest.requireActual

Let’s say we have this module

export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';

We only want to mock it partially, for example we only want to mock the default export and foo but leave bar untouched:

//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
  const originalModule = jest.requireActual('../foo-bar-baz');

  //Mock the default export and named export 'foo'
  return {
    __esModule: true,
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport).toHaveBeenCalled();

  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});

Partial mocking makes sense when you test code that still relies partly on the real implementation of the module. Consider you have this code:

import fetch from 'node-fetch';

export const createUser = async () => {
  const response = await fetch('https://website.com/users', {method: 'POST'});
  const userId = await response.text();
  return userId;
};

You write a test an completely mock node-fetch:

jest.mock('node-fetch');

import fetch, {Response} from 'node-fetch';
import {createUser} from './createUser';

test('createUser calls fetch with the right args and returns the user id', async () => {
  fetch.mockReturnValue(Promise.resolve(new Response('4')));

  const userId = await createUser();

  expect(fetch).toHaveBeenCalledTimes(1);
  expect(fetch).toHaveBeenCalledWith('https://website.com/users', {
    method: 'POST',
  });
  expect(userId).toBe('4');
});

Running the test creates an error TypeError: response.text is not a function. That is because you unintentionally mocked a part of node-fetch that is responsible for properly handling a Response. To solve this, you need to tell Jest to exclude the Response object from mocking. You can do this by adding requireActual:

jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');

Automatic mocks vs Manual mocks

Automatic Mocks with jest.mock()

Advantages:

  1. Ease of Use: It’s convenient to use jest.mock() directly in your test file. You simply specify the module you want to mock, and Jest takes care of replacing all its exports with mock functions.
  2. Granular Control: jest.mock() allows you to mock specific functions or classes within a module, providing granular control over what gets mocked and what doesn’t.
  3. Dynamic Mocking: You can dynamically adjust the behavior of mocked functions using mockReturnValue(), mockResolvedValue(), etc., based on your test scenarios.
  4. Local Scope: Mocks applied with jest.mock() are local to the test file where they’re defined, making it easier to manage and reason about the mocks in each test.

Disadvantages:

  1. Verbose Setup: In some cases, setting up mocks using jest.mock() can be verbose, especially when dealing with multiple mocked functions or complex mock configurations.
  2. Less Discoverable: Since mocks are defined within the test file, it may be less discoverable for other developers who are not familiar with the test suite.

Manual Mocks with __mocks__ Folder

Advantages:

  1. Separation of Concerns: Manual mocks in the __mocks__ folder keep your test code cleaner and separate from your source code, promoting a clearer separation of concerns.
  2. Global Configuration: Mocks defined in the __mocks__ folder are global, meaning they apply across all test files that import the mocked module. This can be advantageous for ensuring consistent behavior across tests.
  3. Simplified Test Code: Test files become simpler as they don’t need to define mocks explicitly. The mocked implementations are automatically applied when importing the module.

Disadvantages:

  1. Less Granular Control: Manual mocks may not offer as granular control over mocking specific functions or classes within a module compared to jest.mock().
  2. Potential Overhead: The __mocks__ folder might introduce overhead in larger projects, especially if multiple modules require manual mocks, potentially cluttering the project structure.
  3. Global Scope: Since mocks defined in the __mocks__ folder are global, they can lead to unintended consequences if not used carefully, potentially affecting unrelated tests.

In summary, choosing between automatic mocks with jest.mock() and manual mocks using the __mocks__ folder depends on factors like project size, complexity, and developer preference. jest.mock() offers more granular control and local scope, while __mocks__ provides a cleaner separation of concerns and global configuration. Consider these factors when deciding which approach best suits your testing needs.

About Author

Mathias Bothe To my job profile

I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.