DEV Community

Mike Borozdin
Mike Borozdin

Posted on • Originally published at mikeborozdin.com

Change Jest mock per test with ease

Changing implementation of Jest mocks per test can be confusing. This blog post will present a simple solution for that. You'll see how each test can get its own mock for both constant values and functions. The solution doesn't rely on using require().

Sample app

Let's start with an example - we have a function - sayHello(name) - it prints out Hi, ${name}. And depending on configuration it either capitalizes the name or not.

say-hello.js

import { CAPITALIZE } from './config';

export const sayHello = (name) => {
  let result = 'Hi, ';

  if (CAPITALIZE) {
    result += name[0].toUpperCase() + name.substring(1, name.length);
  } else {
    result += name;
  }

  return result;
};
Enter fullscreen mode Exit fullscreen mode

And we want to test its behaviour like this:

say-hello.js

import { sayHello } from './say-hello';

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    expect(sayHello('john')).toBe('Hi, john');
  });
});
Enter fullscreen mode Exit fullscreen mode

One of those tests is bound to fail. Which one - depends on the value of CAPITALIZE.

Setting a value inside jest.mock() will not help either. It will be the same as relying on the hardcoded value - one of the tests will fail.

jest.mock('./config', () => ({
  CAPITALIZE: true // or false
}));
Enter fullscreen mode Exit fullscreen mode

Changing mock of non-default const

So we need to change the mock of a non-default const.

First, let's change the way we mock the config module:

jest.mock('./config', () => ({
  __esModule: true,
  CAPITALIZE: null
}));
Enter fullscreen mode Exit fullscreen mode

We do set CAPITALIZE to null, because we'll set its real value in the individual tests. We also have to specify __esModule: true, so that we could correctly import the entire module with import * as config.

Next step is we need to import the module:

import * as config from './config';
Enter fullscreen mode Exit fullscreen mode

And finally change the mock value in each test:

import { sayHello } from './say-hello';
import * as config from './config';

jest.mock('./config', () => ({
  __esModule: true,
  CAPITALIZE: null
}));

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    config.CAPITALIZE = true;

    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    config.CAPITALIZE = false;

    expect(sayHello('john')).toBe('Hi, john');
  });
});
Enter fullscreen mode Exit fullscreen mode

How does it work?

jest.mock() replaces the entire module with a factory function we provide in its second argument. So when we import that module we get a mock instead of the real module. That also means that we can import the same module in the test itself. And that will give us access to the mock which behaviour we can change.

Why import entire module versus just the const we need?

Why can't we just import in this way import CAPITALIZE from './config';? If we import it in that way, we won't be able to re-assign a value to it. Values are always imported as constants.

TypeScript

If you're using TypeScript the line where you're changing the mock:

config.CAPITALIZE = true;
Enter fullscreen mode Exit fullscreen mode

will give you an error:

Cannot assign to 'CAPITALIZE' because it is a read-only property
Enter fullscreen mode Exit fullscreen mode

That's because TypeScript treats imports as constants and objects with read-only properties.

We can fix that by type casting to an object with writeable properties, e.g.:

import * as config from './config';

const mockConfig = config as { CAPITALIZE: boolean };

// and then in a test
mockConfig.CAPITALIZE = true;
Enter fullscreen mode Exit fullscreen mode

Changing mock of export default const

Okay, but what if we need to change the mock of a value that is a default export of the module?

const CAPITALIZE = true;

export default CAPITALIZE;
Enter fullscreen mode Exit fullscreen mode

We can use the same approach, we just need to mock the default attribute:

import { sayHello } from './say-hello';
import * as config from './config';

jest.mock('./config', () => ({
  __esModule: true,
  default: null
}));

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    config.default = true;

    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    config.default = false;

    expect(sayHello('john')).toBe('Hi, john');
  });
});
Enter fullscreen mode Exit fullscreen mode

TypeScript

As with mocking a constant that is non-default export, we need to type cast the imported module into an object with writeable properties

We can fix that by type casting to an object with writeable properties. This time though we change the default attribute instead of CAPITALIZE.

import * as config from './config';

const mockConfig = config as { default: boolean };

// and then in a test
mockConfig.default = true;
Enter fullscreen mode Exit fullscreen mode

Changing mock of non-default function

What if the configuration is returned by a function instead of a constant:

const CAPITALIZE = true;

export default CAPITALIZE;
Enter fullscreen mode Exit fullscreen mode

Actually, it'll be even more straightforward than dealing with constants, as we don't need to import the entire module via import * as entireModule and as a result we won't have to provide __esModule: true.

Our test will simply looks like this:

import { sayHello } from './say-hello';
import { shouldCapitalize } from './config';

jest.mock('./config', () => ({
  shouldCapitalize: jest.fn()
}));

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    shouldCapitalize.mockReturnValue(true);

    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    shouldCapitalize.mockReturnValue(false);

    expect(sayHello('john')).toBe('Hi, john');
  });
});
Enter fullscreen mode Exit fullscreen mode

TypeScript

This line

shouldCapitalize.mockReturnValue(false);
Enter fullscreen mode Exit fullscreen mode

will give a TypeScript error of:

Property 'mockReturnValue' does not exist on type '() => boolean'.
Enter fullscreen mode Exit fullscreen mode

Indeed, TypeScript thinks we've imported a function that returns a boolean, not a Jest mock.

We can correct it again with type casting to a Jest mock.

import { shouldCapitalize } from './config';

const mockShouldCapitalize = shouldCapitalize as jest.Mock;

// and then in a test
mockConfig.default = true;
Enter fullscreen mode Exit fullscreen mode

Changing mock of default function

There might also be a case that we want to change the behaviour of the function that is the default export of a module.

const shouldCapitalize = () => true;

export default shouldCapitalize;
Enter fullscreen mode Exit fullscreen mode
In that case, we employ a technique similar mocking default constants - we'll mock `default`, set `__esModule: true` and will import the entire module with `*`.

import { sayHello } from './say-hello';
import * as config from './config';

jest.mock('./config', () => ({
  __esModule: true,
  default: jest.fn()
}));

describe('say-hello', () => {
  test('Capitalizes name if config requires that', () => {
    config.default.mockReturnValue(true);

    expect(sayHello('john')).toBe('Hi, John');
  });

  test('does not capitalize name if config does not require that', () => {
    config.default.mockReturnValue(false);

    expect(sayHello('john')).toBe('Hi, john');
  });
});
Enter fullscreen mode Exit fullscreen mode

TypeScript

Similar to mocking a non default function, we need to type cast the imported module into an object with writeable properties

import * as config from './config';

const shouldCapitalizeMock = config.default as jest.Mock;

// and in a test
shouldCapitalizeMock.mockReturnValue(true);
Enter fullscreen mode Exit fullscreen mode

Conclusion

All examples above rely on a simple premise that:

  • jest.mock() mocks a specific module (unsurprisingly, huh?)
  • So everywhere you import it you'll get a mock instead of a real module
  • And that applies to tests, as well
  • So import mocked modules in test and change their implementation

Latest comments (0)