Mastodon

Quickening: Mocking in Jest

Brian and I sat down this morning and it took us an hour and a half to bang out two solid tests using Jest's mocking capabilities. When I realized that he and I together represent 40 years of software development experience I thought "should it have taken us that long to write two simple tests?"

The answer is "no" so I wanted to put what we learned out there for people who have been programming a while but maybe don't have a ton of experience using this particular JavaScript testing framework.

On with the show.

If you want to mock a property of an object you're injecting into code

Easiest case: the thing you're testing accepts its dependency as an argument. Here's how you do it:

// src/MyModule.js
class MyModule {
    constructor(adder) {
        this.adder = adder
    }

    add(a, b) {
        return this.adder.sumUp(a, b)
    }
}

export default MyModule

// test/MyModule.spec.js
import MyModule from '../src/MyModule'

describe('MyModule', () => {
    it('should call the injected adder\'s "sumUp" function', () => {
        const mockAdder = {
            sumUp: jest.fn()
        }
        const module = new MyModule(mockAdder)

        module.add(4, 5)

        expect(mockAdder.sumUp).toBeCalledTimes(1)
        expect(mockAdder.sumUp).toBeCalledWith(4, 5)
    })
})

The magic here is on line 20. jest.fn() creates a mock function within mockAdder, which we can easily inject through the constructor. We then assert some expectations on mockAdder.sumUp directly because it's a mock.

If you want to mock a property on an object that is imported from a module

Slightly more complicated, you've imported a module in the thing you're testing and you want it to be a mock:

// src/MyDependency.js
const MyDependency = {
    logMultiplication(a, b, answer) {
        console.log(`multiplied ${a} * ${b} to get ${answer}`)
    }
}

export default MyDependency

// src/MyModule.js
import MyDependency from './MyDependency'

class MyModule {
    multiply(a, b) {
        const answer = a * b
        MyDependency.logMultiplication(a, b, answer)
        return answer
    }
}

export default MyModule

// test/MyModule.spec.js
import MyModule from '../src/MyModule'
import MyDependency from '../src/MyDependency'

jest.mock('../src/MyDependency')

describe('MyModule', () => {
    afterAll(() => {
        jest.resetAllMocks()
    })
    
    it('should call MyDependency.logMultiplication', () => {
        const module = new MyModule()

        const answer = module.multiply(4, 5)

        expect(answer).toBe(20)
        expect(MyDependency.logMultiplication).toBeCalledTimes(1)
        expect(MyDependency.logMultiplication).toBeCalledWith(4, 5, 20)
    })
})

This example is a little trickier. Look at line 27.

When you use jest.mock Jest will scan through MyModule and replace every property with an instance of jest.fn() (at least, that's accurate enough for a good mental model). That way you don't have to manually inject the mock(s) into the object under test, but you can still assert on the mocks using expect (as in lines 40-41).

Now for the tricky one:

If you want to mock a module that is imported as a function

If you're in react space you might want to mock out a hook, which is imported as a function. Here's how you test that (but with a non-react functional import, because I don't want to clutter the test with react-specific test code):

// src/actuallyTransfer.js
const actuallyTransferMoneyFromMyBankAccount = async (otherAccount, amount) => {
    // yeah, we'll probably want to mock this one out instead of
    // running it every time we do a test
    const result = await fetch('https://api.actualbank.com/v2/transfer', {
        method: 'POST',
        body: {
            fromAccount: process.env('MY_ACCOUNT_NUMBER'),
            toAccount: otherAccount,
            amount
        }
    })

    return result.ok
}

export default actuallyTransferMoneyFromMyBankAccount

// src/MyModule.js
import actuallyTransferMoneyFromMyBankAccount from './actuallyTransfer'

class MyModule {
    multiplyThenTransferResultToAccount(a, b, accountNumber) {
        const result = a * b
        const didSucceed = actuallyTransferMoneyFromMyBankAccount(accountNumber, result)
        return didSucceed ? result : -1
    }
}

export default MyModule

// test/MyModule.spec.js
import MyModule from '../src/MyModule'
import actuallyTransferMoneyFromMyBankAccount from '../src/actuallyTransfer'

jest.mock('../src/actuallyTransfer')

describe('MyModule.multiplyThenTransferResultToAccount', () => {
    afterAll(() => {
        jest.resetAllMocks()
    })

    it('should call actuallyTransfer when doing a multiplication operation', () => {
        const sampleAccountNumber = 12345  // same as the combination on my luggage
        const module = new MyModule()

        module.multiplyThenTransferResultToAccount(4, 5, sampleAccountNumber)

        expect(actuallyTransferMoneyFromMyBankAccount).toHaveBeenCalledTimes(1)
        expect(actuallyTransferMoneyFromMyBankAccount).toHaveBeenCalledWith(sampleAccountNumber, 20)
        // you can also expect this way:
        expect(actuallyTransferMoneyFromMyBankAccount.mock.calls.length).toBe(1)
        expect(actuallyTransferMoneyFromMyBankAccount.mock.calls[0]).toEqual([sampleAccountNumber, 20])
    })

    it('should return actual answer if transfer succeeds', () => {
        const module = new MyModule()
        actuallyTransferMoneyFromMyBankAccount.mockReturnValue(true)

        const result = module.multiplyThenTransferResultToAccount(4, 5)

        expect(result).toBe(20)
    })

    it('should return -1 if transfer fails', () => {
        const module = new MyModule()
        actuallyTransferMoneyFromMyBankAccount.mockReturnValue(false)

        const result = module.multiplyThenTransferResultToAccount(4, 5)

        expect(result).toBe(-1)
    })
})

You have your same jest.mock call on line 36, but since that module exports a function Jest will quietly replace it with a jest.fn so you can set up the return behavior like on lines 58 and 67.

Also check out the alternate assertion syntax on lines 52-53, in case you need something more fine-grained.

Get out there and mock

I've intentionally kept this article brief so I could get the information out there and not feel I needed to pretty up the prose. If you see any issues, please reach out on twitter and let me know. If you're curious about the docs (which is where Brian and I started, but the examples were hard for us to map to our context), check them out here.

Mock on.