Change Jest mock per test with ease
Tags: javascript, testing, tutorial, webdev
Published: 2021-02-27 11:11:52

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**

```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;
};
```

And we want to test its behaviour like this:

**say-hello.js**

```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');
  });
});
```

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.

```js
jest.mock('./config', () => ({
  CAPITALIZE: true // or false
}));
```

# 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:

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

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:

```js
import * as config from './config';
```

And finally change the mock value in each test:

```js
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');
  });
});
```

## 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:

```ts
config.CAPITALIZE = true;
```

will give you an error:

```ts
Cannot assign to 'CAPITALIZE' because it is a read-only property
```

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.:

```ts
import * as config from './config';

const mockConfig = config as { CAPITALIZE: boolean };

// and then in a test
mockConfig.CAPITALIZE = true;
```

# 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?

```js
const CAPITALIZE = true;

export default CAPITALIZE;
```

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

```js
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');
  });
});
```

## 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`.

```ts
import * as config from './config';

const mockConfig = config as { default: boolean };

// and then in a test
mockConfig.default = true;
```

# Changing mock of non-default function

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

```js
const CAPITALIZE = true;

export default CAPITALIZE;
```

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:

```js
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');
  });
});
```

## TypeScript

This line 

```ts
shouldCapitalize.mockReturnValue(false);
```

will give a TypeScript error of:

```ts 
Property 'mockReturnValue' does not exist on type '() => boolean'.
```

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.

```ts
import { shouldCapitalize } from './config';

const mockShouldCapitalize = shouldCapitalize as jest.Mock;

// and then in a test
mockConfig.default = true;
```

# 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.

```js
const shouldCapitalize = () => true;

export default shouldCapitalize;
```

```js
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');
  });
});
```

## TypeScript

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

```js
import * as config from './config';

const shouldCapitalizeMock = config.default as jest.Mock;

// and in a test
shouldCapitalizeMock.mockReturnValue(true);
```

# 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