omer727
omer727

Reputation: 7565

mock one function using jest.mock() and not another in the same file

I've the following code

main.ts

export const add = (x: number, y: number): number => {
    return x + y;
}

export const minus = (x: number, y: number): number => {
    return x - y;
}

export const calculate = (x: number, y: number, operator: string) => {
    let result = -1;
    switch (operator) {
        case '+':
            result = add(x, y);
            break;
        case '-':
            result = minus(x, y);
            break;
    }
    return result;
}

I want to write the following test

main.test.ts

import {calculate} from "./main";

jest.mock("./main", () => ({
    ...jest.requireActual("./main"),
    add: function () {
        return 17;
    }
}));

describe('testing', function () {
    it('should test', function () {
        const result = calculate(3, 2, '+');
        expect(result).toBe(5);
    });
});

I want to do partial mocking, where I mock the add() function but I don't want to use the actual implementation for calculate(). But it doesn't work for me, add(3,2) just returns 5, instead of 17. I guess because they are both from the same module.

I know I can use jest.spyOn() but I wanted to understand why jest.mock() wouldn't work and how to fix it

Upvotes: 4

Views: 9269

Answers (1)

TmTron
TmTron

Reputation: 19401

mock will not replace the original source-code file, but only add a new mocked version.

import { add } from './main';

jest.mock('./main', () => {
  const actual = jest.requireActual('./main');
  return {
    // ...actual,
    add: function () {
      return 17;
    }
  };
});

describe('add', () => {
  it('should use the mocked add function', () => {
    expect(add(1, 2)).toBe(17);
  });

  it('should use the original add function', async () => {
    const main = jest.requireActual('./main');
    expect(main.add(1, 2)).toBe(3);
  });
});

In your mock-factory function you replace add, but you use the original calculate function, which calls the original add function. This is why the mocked add function is not used.

To make it work the way you want, you also have to replace calculate (so that it can use the new add function), e.g.:

import { add, calculate, minus } from './main';

jest.mock('./main', () => ({
  //__esModule: true,
  ...jest.requireActual('./main'),
  calculate: function (x: number, y: number, operator: string) {
    let result = -1;
    switch (operator) {
      case '+':
        result = add(x, y);
        break;
      case '-':
        result = minus(x, y);
        break;
    }
    return result;
  },
  add: function () {
    return 17;
  }
}));

describe('calculate', () => {
  it('should use the mocked calculate and add function', () => {
    const result = calculate(3, 2, '+');
    expect(result).toBe(17);
  });
});

Since all these replacements are confusing and error prone, you should really use jest.spyOn instead.
Quote from this answer

Mutating an imported module is nasty and can lead to side effects like tests that pass or fail depending on execution order.

side note

If you really want to go this way, then you should at least move the operators and the calculate function to different files. Then you only need to mock the operators module.

operators.ts:

export function add(x: number, y: number): number {
  return x + y;
}

export const minus = (x: number, y: number): number => {
  return x - y;
};

calculate.ts:

import { add, minus } from './operators';

export const calculate = (x: number, y: number, operator: string) => {
  let result = -1;
  switch (operator) {
    case '+':
      result = add(x, y);
      break;
    case '-':
      result = minus(x, y);
      break;
  }
  return result;
};

calculate.spec.ts:

import { calculate } from './calculate';

jest.mock('./operators', () => ({
  //__esModule: true,
  ...jest.requireActual('./operators'),
  add: function () {
    return 11;
  }
}));

describe('calculate', () => {
  it('should use the mocked add function', () => {
    const result = calculate(3, 2, '+');
    expect(result).toBe(11);
  });
});

Upvotes: 3

Related Questions