Skip Main Navigation
Ben IlegboduBen Ilegbodu

Using Jest mock functions in TypeScript

How to represent an imported function in TypeScript that has been replaced by a Jest mock

Sunday, April 25, 2021 · 5 min read

I write my unit tests in TypeScript because I want to ensure that my tests are also type checked. This not only prevents me from cutting corners in my tests by only passing the data I "know" I need, but also ensures that when I refactor my code my tests still are correct. However, we do a lot of funky things in our tests (like mocking functions) which makes using TypeScript more challenging and/or frustrating.

In order to aid TypeScript in understanding Jest mocks, we can use the jest.MockedFunction type that's available in the @types/jest DefinitelyTyped package (from version 24.9.0).

// src/api/players.test.ts

import { getPlayerLadder } from './players'
import {
  MOCK_VALID_LADDER_ID,
  MOCK_VALID_LADDER,
  MOCK_PLAYER_IDS,
  MOCK_PLAYERS,
} from './mocks'
import { getLadder, getPlayers } from '../db/firestore'

// mock the firestore module with an auto-mocked version.
// `getLadder` & `getPlayers` will be jest mocks
jest.mock('../db/firestore')

// take the functions we're mocking and add the jest mock properties
// to them so that everything will type-check properly
const mockGetLadder = getLadder as jest.MockedFunction<typeof getLadder>
const mockGetPlayers = getPlayers as jest.MockedFunction<typeof getPlayers>

describe('getPlayerLadder', () => {
  it('returns a player ladder from a valid id', async () => {
    // use the variables that are typed with the additional
    // mock information instead of the originals
    mockGetLadder.mockResolvedValue(MOCK_VALID_LADDER)
    mockGetPlayers.mockResolvedValue(MOCK_PLAYERS)

    const playerLadder = await getPlayerLadder(MOCK_VALID_LADDER_ID)

    expect(getLadder).toHaveBeenCalledTimes(1)
    expect(getLadder).toHaveBeenCalledWith(MOCK_VALID_LADDER_ID)

    expect(getPlayers).toHaveBeenCalledTimes(1)
    expect(getPlayers).toHaveBeenCalledWith(MOCK_PLAYER_IDS)

    expect(playerLadder).toMatchSnapshot()
  })
})

If you've been dealing with this problem and you're already familiar with how Jest mock functions work in JavaScript, this may be all you needed in order to solve your problem. If that's the case, I'm glad I was able to help! 😄 But if you want a better understanding of how this all works, let's keep the party going!

In my latest dev project NBA Player Tiers, I have this API function called getPlayerLadder.

// src/api/players.ts

import { getLadder, getPlayers } from '../db/firestore'
import { getPlayerIdsFromLadder, makePlayerLadder } from '../utils'
import type { LadderId } from '../types'

export const getPlayerLadder = async (id: LadderId) => {
  // make a request to firestore to grab raw ladder
  // that just has player IDs
  const rawLadder = await getLadder(id)

  const playerIds = getPlayerIdsFromLadder(rawLadder)

  // make another request to firestore to grab all of the
  // full player info for each player ID
  const players = await getPlayers(playerIds)

  // replace each ID w/ the corresponding player info to
  // create a player ladder
  const playerLadder = makePlayerLadder(rawLadder, players)

  return playerLadder
}

What getPlayerLadder does isn't terribly important, but I just wanted to provide something concrete as we work through a test.

// src/api/players.test.ts

import { getPlayerLadder } from './players'
import {
  MOCK_VALID_LADDER_ID,
  MOCK_VALID_LADDER,
  MOCK_PLAYER_IDS,
  MOCK_PLAYERS,
} from './mocks'
import { getLadder, getPlayers } from '../db/firestore'

// mock the firestore module with an auto-mocked version
// `getLadder` & `getPlayers` will be jest mocks
jest.mock('../db/firestore')

describe('getPlayerLadder', () => {
  it('returns a player ladder from a valid id', async () => {
    // have `getLadder` & `getPlayers` to return mocked data.
    // The test passes, but these two lines will be type errors in TypeScript 😢
    getLadder.mockResolvedValue(MOCK_VALID_LADDER)
    getPlayers.mockResolvedValue(MOCK_PLAYERS)

    const playerLadder = await getPlayerLadder(MOCK_VALID_LADDER_ID)

    expect(getLadder).toHaveBeenCalledTimes(1)
    expect(getLadder).toHaveBeenCalledWith(MOCK_VALID_LADDER_ID)

    expect(getPlayers).toHaveBeenCalledTimes(1)
    expect(getPlayers).toHaveBeenCalledWith(MOCK_PLAYER_IDS)

    expect(playerLadder).toMatchSnapshot()
  })
})

Because getLadder & getPlayers are making requests to the Firebase Cloud Firestore (a NoSQL database in the cloud), we need to mock those functions to return mocked data. We definitely don't want to be making real API requests in our tests. Mocking is fine in Jest, but calling .mockResolvedValue on the mocked getLadder & getPlayers functions cause type errors. These type errors happen because TypeScript doesn't understand what jest.mock(...) is doing. We get an error that looks like:

Property 'mockResolvedValue' does not exist on type '(id: LadderId) => RawLadder'.

I've always been adamant about type-checking my tests, but in the past I would use // @ts-ignore comments. I didn't know how to fix the type error, but at least the rest of tests were still type-checked:

// @ts-ignore getLadder is a mock for testing purposes
getLadder.mockResolvedValue(MOCK_VALID_LADDER)
// @ts-ignore getPlayers is a mock for testing purposes
getPlayers.mockResolvedValue(MOCK_PLAYERS)

I use the ban-ts-comment ESLint rule from @typescript-eslint/eslint-plugin which required me to include a description for why I'm using // @ts-ignore. Recently, though, I got tired of using // @ts-ignore and searched for a more legit way to solve this problem. I wanted to be able to let TypeScript know that getLadder & getPlayers were in fact Jest mock functions in addition to having their normal API.

It turns out that the @types/jest DefinitelyTyped package includes a type to solve this: jest.MockedFunction. It takes any function and extends it with the jest.MockInstance interface. We can use it to type our mocked functions.

import { getPlayerLadder } from './players'
import {
  MOCK_VALID_LADDER_ID,
  MOCK_VALID_LADDER,
  MOCK_PLAYER_IDS,
  MOCK_PLAYERS,
} from './mocks'
import { getLadder, getPlayers } from '../db/firestore'

jest.mock('../db/firestore')

const mockGetLadder = getLadder as jest.MockedFunction<typeof getLadder>
const mockGetPlayers = getPlayers as jest.MockedFunction<typeof getPlayers>

// tests...

We take our mocked functions, getLadder & getPlayers and use type assertions to create new variables with the extended types. The mocked functions are still the same, but they now have the full type information.

describe('getPlayerLadder', () => {
  it('returns a player ladder from a valid id', async () => {
    // use the variables that are typed with the mock information
    // instead of the originals so that they pass type-checking
    mockGetLadder.mockResolvedValue(MOCK_VALID_LADDER)
    mockGetPlayers.mockResolvedValue(MOCK_PLAYERS)

    const playerLadder = await getPlayerLadder(MOCK_VALID_LADDER_ID)

    expect(getLadder).toHaveBeenCalledTimes(1)
    expect(getLadder).toHaveBeenCalledWith(MOCK_VALID_LADDER_ID)

    expect(getPlayers).toHaveBeenCalledTimes(1)
    expect(getPlayers).toHaveBeenCalledWith(MOCK_PLAYER_IDS)

    expect(playerLadder).toMatchSnapshot()
  })
})

So now when we use mockGetLadder & mockGetPlayers in our tests, they finally type-check. 🎉

I went on this long roundabout journey to figure out jest.MockedFunction, reading the source code of both @types/jest and ts-jest (an alternative way to use TypeScript with Jest). ts-jest has this test helper called mocked() which ultimately does what we did with jest.MockedFunction, but nicely abstracted away in a helper. It was looking at the mocked() implementation and mapping it back to @types/jest that I figured out jest.MockedFunction.

It really only took a couple of hours of code spelunking to figure this out, but it turns out the answer had been right under my nose the whole time. The official Jest docs added instructions on using TypeScript with mock functions at some point and the solution was exactly what I had "discovered." 😅

But it wasn't a total waste of time because I cribbed mocked() from ts-jest to create my own asMock() helper.

// test/helpers.ts

type MockableFunction = (...args: any[]) => any

// use generic constraints to restrict `mockedFunc` to be any type of function
export const asMock = <Func extends MockableFunction>(mockedFunc: Func) =>
  mockedFunc as jest.MockedFunction<typeof mockedFunc>

As you can see asMock doesn't do anything with mockedFunc. It simply returns it with the proper type information. I don't need to mock functions all that often. But I figure wrapping the type assertion in a helper means most folks won't have to try to understand all the jest.MockedFunction<> business in their test code.

// src/api/players.test.ts

import { getPlayerLadder } from './players'
import {
  MOCK_VALID_LADDER_ID,
  MOCK_VALID_LADDER,
  MOCK_PLAYER_IDS,
  MOCK_PLAYERS,
} from './mocks'
import { getLadder, getPlayers } from '../db/firestore'
import { asMock } from '../../test/helpers'

// mock the firestore module with an auto-mocked version
// `getLadder` & `getPlayers` will be jest mocks
jest.mock('../db/firestore')

// take the functions we're mocking and add the jest mock properties
// to them so that everything will type-check properly
const mockGetLadder = asMock(getLadder)
const mockGetPlayers = asMock(getPlayers)

// tests...

I chose the name asMock for the function to try to convey that the function only does a type assertion. It doesn't actually do any of the mocking. That's what jest.mock() does.

Anyway, I'm curious if there are other folks who have dealt with this problem? What sorts of workarounds were you using? Feel free to reach out to me on Twitter at @benmvp!

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub