How to mock class constructor with parameters- Jest and TypeScript

Earlier I wrote How to Mock a TypeScript class or dependency using Jest, that is basically a how to auto mock the ES class or module when it comes to typescript. Since it happen to come up on google results, get to see what people search around the subject and “jest mock constructor with parameters typescript” is a popular one! I also I get lots of question about both manual mock and mocking a class constructor with parameters.

In this article we are going to expand our knowledge and review how to manually mock a ES class or module with Jest especially when constructor has arguments. This applies to both JavaScript and typescript.

Mock constructor

Before we get to manual mocking the class, let’s get the constructor parameter problem out of the way. The short answer to how to mock constructor with parameters in jest is : parameters do not really matter, you don’t need to tell your manual mock about the arguments; Pretend that your contractor doesn’t have parameters and you are good with your mock! Just assert that the constructor is called with correct value. But why not reading the long answer?

I am going to teach you how to access the argument that used to new up your class form your mock, but first hear me out! If you found yourself in a situation that you need to access that parameter in your mock, you are probably doing something wrong!

Mock class constructor with parameters

When you feel you need to mock the constructor with arguments, you are probably trying to influence the library to do something! Or maybe you are trying to duplicate the logic. Remember that we are not trying to test the library we are mocking! What you need to do is to create a similar class with absolutely no logic that implements the same APIs (Application Programming Interface), so you can replace it with the original one. Regardless of what inputs it gets to its public functions, it always returns the same predetermined results.

Let me explain with a simple example! Assume we have a class or module called miniMath that gets an argument and has public functions of double() and tipple() and isEven() :

class MiniMath {
    constructor(foo) {
        this.foo = foo;
    }
    double = () => //call some external system to calculate double foo
    tipple = () => //call some external system to calculate tipple foo
    isEven = () => //call some external system to calculate if foo is Even 
}

And we have a very simple function called myCode. That function internally uses MiniMath and you want to test this function (your SUT) :

function myCode(bar: number) {
    const baz= bar+1
    const math = new MiniMath(baz);
    if (math.isEven())
        return math.double() - 1
    return math.tipple()
}

While unit testing myCode function, You probably are tempted to write a Jest test and mock constructor with parameters. In that case, you may want create a mockedMiniMath to mimic the logic of the MiniMath to skip the external calls. So, in that case, MiniMath(2).isEven() returns true and MiniMath(3).isEven() returns false. Same with double, and tipple functions. Although it sounds like a solid plan, I strongly recommend not to make any kind of logic in your mocks.

A better approach is to write a test that mocks the whole MiniMath class without any logic. This means it doesn’t really matter how you Initialize your MiniMath, all the functions always return the same results for each test case. I.e. MiniMath(2).isEven() === MiniMath(3).isEven() for the same test case. But it does not make any sense does it?

Stupid mocks are desired!

Let me elaborate! The function that says both 2 and 3 are even is stupid! And that is what we really want our mocks to be, we want “stupid” mocks! Here is the reason:

Readable tests : code is more for human to be read than the machine! When it comes to tests, this amplifies. Have you reviewed a pull request that developer deleted a failing test just because it fails? and most of the times developers has no idea what that test really tests? When you put logic into your mocks, you make your tests way harder to understand. If your reader need to put effort to understand your mock, you are doing it wrong .

Who tests your mock: anything other than whatever input => same output is a code that needs to be tested. You might totally write wrong logic for your mock and it says 3 is even, then you test your whole function based on that wrong logic!

Go straight to the point of failure : when you are not writing logic in your mock , you don’t need to care about the parameters of your constructor any more. What you care is if the constructor is called with a certain value. Here is an example (the code version is on the next section, in case it is hard to understand the text):

When you call myCode(3) (your own function that you are testing) as SUT you should expect that your MiniMath constructor to be called with value 2 (that is what you expect from your logic based on your actual requirements) and expect your SUT return to 7. The important thing to understand is :

When you are mocking for testing this this path of your function, the isEven() is always true and double() is always 8 and you don’t need to care about tipple() at this point.

So what if you want to test with myCode(3) and myCode(5) – that both are exactly same path- and you expect myCode() to return a different value? In that case your value only changes based on internal calculations of the MiniMath; so, you are not testing your SUT anymore, you are testing your mock!

Your next test though would be SUT=myCode(4), isEven() always false and tipple() always 9. This is the other path of your function. in this case you expect constructor to be called with parameter value of 3 (baz) and also expect SUT return 9 (tipple buz).

see the Recommended way of manually mocking class or module code below.

Anti pattern way of mocking constructor

If you consider above you never need to mock the constructor! But just to demonstrate, let’s compare the anti pattern way of testing with recommended one.

import { myCode } from "./my-code";

// This is anti pattern, please do not do this
jest.mock('./mini-math', () => ({
    __esModule: true, //When module has multiple exports
    default: jest.fn().mockImplementation((param) => {
        return {
            double: jest.fn().mockImplementation(() => param * 2),
            tipple: jest.fn().mockImplementation(() => param * 3),
            isEven: jest.fn().mockImplementation(() => param % 2 === 0)
        }
    })
}));

it('calls myCode with odd input', () => {
    const sut = myCode(3);
    expect(sut).toBe(7);
})

it('calls myCode with even input', () => {
    const sut = myCode(2);
    expect(sut).toBe(9);
})

As you can see line 6 to 11 the logic of the MiniMath is duplicated: the param will contain the value passed to the constructor and functions has logics depend on value of the param. Remember that this way of mocking class with constructor it is an anti pattern. Moreover, although the test cases actually tests all the code, it doesn’t tell you that much about what happens in system under the test. You might need to run the code and debug it to see what is happening.

Recommended way of manually mocking class or module

Now let’s take a look at the recommended way mocking class or module that has a contractor with parameters.

import MiniMath from "./mini-math";
import { myCode } from "./my-code";
let miniMathValues = {
    double: 0,
    tipple: 0,
    isEven: false
}

jest.mock('./mini-math', () => ({
    __esModule: true, //When module has multiple exports 
    default: jest.fn().mockImplementation(() => {
        return {
            double: jest.fn().mockImplementation(() => miniMathValues.double),
            tipple: jest.fn().mockImplementation(() => miniMathValues.tipple),
            isEven: jest.fn().mockImplementation(() => miniMathValues.isEven)
        }
    })
}));

const mockedMiniMath = jest.mocked(MiniMath, true);
beforeEach(() => {
    mockedMiniMath.mockClear();
});

it('calls myCode with odd input', () => {
    miniMathValues = {
        ...miniMathValues,
        double: 8,
        isEven: true, //myCode() adds 1
    }
    const sut = myCode(3);
    expect(mockedMiniMath).toBeCalledWith(4);
    expect(mockedMiniMath).toBeCalledTimes(1);
    expect(sut).toBe(7);
})

it('calls myCode with even input', () => {
    miniMathValues = {
        ...miniMathValues,
        tipple: 9,
        isEven: false, //myCode() adds 1
    }
    const sut = myCode(2);
    expect(mockedMiniMath).toBeCalledWith(3);
    expect(mockedMiniMath).toBeCalledTimes(1);
    expect(sut).toBe(9);
});

As you see the values returning from the mock here are decided in advance for each test case. This way you know the values the functions are returning by looking at the test, you do not need to try hard or debug! Moreover really test the path you intended to test without being affected by other external actors (ex value of buz).

Compare two methods

Let’s assume we made a mistake and in line 2 of myCode set the buz=bar instead of baz=bar+1. This makes the input parameter wrong in our mock. In that case both tests going to fail! In case of the first one, it just tell you that the test went wrong but it doesn’t really tell you why. You are on your own to understand the logic of SUT function, understand the -you made- logic of the mock! And then you need to decide if the test is wrong and should be deleted or the logic of myCode is wrong and should be tested… maybe even you decide that the logic of the mock is wrong! Who knows…

expect(received).toBe(expected) // Object.is equality

    Expected: 7
    Received: 9

      15 | it('calls myCode with odd input', () => {
      16 |     const sut = myCode(3);
    > 17 |     expect(sut).toBe(7);
         |                 ^
      18 | })

Meanwhile, the recommended approach takes you directly to the point the mistake has been made! The rest of the function is still ok and you are still testing the path where isEven() returns false ! As soon as you see this error, you should ask yourself why MiniMath constructor expected 3 but received 2? Something should be wrong with my SUT, around value of buz! oh yes baz should be bar+1 !

expect(jest.fn()).toBeCalledWith(...expected)

    Expected: 3
    Received: 2

    Number of calls: 1

      43 |     }
      44 |     const sut = myCode(2);
    > 45 |     expect(mockedMiniMath).toBeCalledWith(3);
         |                            ^
      46 |     expect(mockedMiniMath).toBeCalledTimes(1);
      47 |     expect(sut).toBe(9);
      48 | })


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *