Mock a typescript interface with jest
Asked Answered
Q

4

60

Is it possible to mock a typescript interface with jest?

For example:

import { IMultiplier } from "./IMultiplier";

export class Math {
  multiplier: IMultiplier;

  public multiply (a: number, b: number) {
    return this.multiplier.multiply(a, b);
  }
}

Then in a test:

import { Math } from "../src/Math";
import { IMultiplier } from "../src/IMultiplier";

describe("Math", () => {

    it("can multiply", () => {
        let mathlib = new Math();
        mathlib.multiplier = // <--- assign this property a mock
        let result = mathlib.multiply(10, 2);
        expect(result).toEqual(20);
    });
});

I've tried to create a mock object to satisfy this a number of ways, but none work. For example assigning it this mock:

let multiplierMock = jest.fn(() => ({ multiply: jest.fn() }));

Will produce something along the lines of:

Error - Type 'Mock<{ multiply: Mock<{}>; }>' is not assignable to type 'IMultiplier'.
Quinone answered 31/8, 2018 at 20:27 Comment(4)
How does multiplier get created/assigned within an instance of Math?Coinsure
@brian-lives-outdoors This is obviously a contrived example, but the code base has situations where multiplier would be passed into the constructor of Math and instances where it's assigned to the multiplier property afterwards (like the above test).Quinone
I created a library which allows you to mock out TypeScript interfaces - github.com/marchaos/jest-mock-extended. There didn't seem to be libs that does this cleanly whilst keeping full type safety. It's based loosely on the discussion here -github.com/facebook/jest/issues/7832Pyrogen
One can also cast an empty object to the desired type ({} as MyInterface) and then just add mocks for the properties/methods needed for testing - https://mcmap.net/q/330712/-mocking-stubbing-a-typescript-interface-with-jestPeppery
C
17

The mock just needs to have the same shape as the interface.

(from the docs: One of TypeScript’s core principles is that type-checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”.)

So mathlib.multiplier just needs to be assigned to an object that conforms to IMultiplier.

I'm guessing that IMultiplier from the example looks something like this:

interface IMultiplier {
  multiply(a: number, b: number): number
}

So the example test will work fine by changing the line in question to this:

mathlib.multiplier = {
  multiply: jest.fn((a, b) => a * b)
};
Coinsure answered 4/9, 2018 at 20:10 Comment(3)
Although it's technically true that a mock just needs to have the same shape as the interface, that misses the whole point. The whole point is to have a convenient way to generate a mock given an interface, so that developers don't have to manually create mock classes just to, say, stub out a single function out of a dozen methods every time you need to run a test.Cutie
Not to mention depending on your project's ESLint settings this solution may not work.Janae
You are my hero, your wisdom "mock just needs to have the same shape as the interface" helped me big. The ES6 Jest mock sent me down the wrong path. I am posting my answer for anyone who wants to mock Node OracleDB in Type Script.Circuit
M
6

try out moq.ts library.

import {Mock} from "moq.ts";

const multiplier = new Mock<IMultiplier>()
   .setup(instance => instance.multiply(3, 4))
   .returns(12)
   .object();

let mathlib = new Math();
mathlib.multiplier = multiplier;
Mainsail answered 31/5, 2020 at 21:58 Comment(1)
OP specifically asks about making this happen in Jest. While Moq might be a viable alternative, it's not what OP was asking.Tenerife
K
0

The answer of @Brian Adams doesn't work if multiplier property is a protected property.
In this case we can do something like this:
Target class:

import { IMultiplier } from "./IMultiplier";

export class Math {
  protected multiplier: IMultiplier;

  public multiply (a: number, b: number) {
    return this.multiplier.multiply(a, b);
  }
}

Unit test:

import { Math } from "../src/Math";
import { IMultiplier } from "../src/IMultiplier";

describe("Math", () => {
    class DummyMultiplier implements IMultiplier {
        public multiply(a, b) {
            // dummy behavior
            return a * b;
        }
    }

    class TestableMathClass extends Math {
        constructor() {
            super();
            // set the multiplier equal to DummyMultiplier
            this.multiplier = new DummyMultiplier();
        }
    }

    it("can multiply", () => {
        // here we use the right TestableMathClass
        let mathlib = new TestableMathClass();
        let result = mathlib.multiply(10, 2);
        expect(result).toEqual(20);
    });

    it("can multiply and spy something...", () => {
        // with spy we can verify if the function was called
        const spy = jest
            .spyOn(DummyMultiplier.prototype, 'multiply')
            .mockImplementation((_a, _b) => 0);

        let mathlib = new TestableMathClass();
        let result = mathlib.multiply(10, 2);
        expect(result).toEqual(20);
        expect(spy).toBeCalledTimes(1);
    });
});

If you are working with a private property, maybe you can inject the property. So, in unit test you also can create a dummy behavior and inject its.

Kevakevan answered 9/3, 2021 at 11:6 Comment(0)
C
0

My answer is related in terms of how to mock interface, namespaces with named and default exports with Jest in TypeScript. The key learning to remember as Brian Adams pointed out above "The mock just needs to have the same shape as the interface." that led me to solve my problem. I tried all ES6 Jest Mocking techniques documented but none worked, the below worked like a charm.

Main Code

  import oracledb, { Connection } from 'oracledb';
  
  public static async getConnection(): Promise<Connection> {
    let connection: Connection;
    try {
      console.log('Getting connection...');
      connection = await oracledb.getConnection('yourPoolAlias');
    } catch (err) {
      console.error('Error while creating connection', err);
      throw err;
    }
    return connection;
  }

Test Code

const mockConnection= {mockConnObj:true};
const mockGetConnection= jest.fn().mockResolvedValue(mockConnection);
let actual= {};

beforeAll(async () => {
      jest.mock('oracledb');
  oracledb.getConnection= mockGetConnection;
  (actual as Connection)= await ConnectionPool.getConnection();
    });

    it('should call oracledb.getConnection with my pool alias', () => {
      expect(oracledb.getConnection).toHaveBeenCalledWith('yourPoolAlias);
    });
Circuit answered 10/4 at 21:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.