DEV Community

Cover image for Partial: how NOT to mock the whole world
Anton Korzunov
Anton Korzunov

Posted on • Updated on

Partial: how NOT to mock the whole world

Mocking is a very controversial concept in testing - some thought leaders advocate it as something "bad"😤, while others have to use mocks🙃 one way or another.
No matter what we think about it - mocks are around.

However, mocks can be different

  • they could be network mocks (use msw)
  • or they could be state mock injected by jest.mock or any other force (I would recommend magnetic-di)

But in all cases, there is a hill to die on - are you going to mock the entire universe 🛸 or just a little bit?

Let's start with the 🪐 entire universe

Jest.mock

jest.mock(or vi.mock) is a good example of "everything" - this helper mocks the entire module providing "empty stubs" for the original content.

Imagine a use case

// index.ts
import {getConfiguration} from './config';

export const isAdmin = () => getConfiguration().user.isAdmin;
Enter fullscreen mode Exit fullscreen mode
// index.spec.ts
import {getConfiguration} from './config';
import {isAdmin} from './';

jest.mock('./config');

test('isAdmin', () => {
  // ??? 🤷‍♂️
  expect(isAdmin()).toBe(true); 
});
Enter fullscreen mode Exit fullscreen mode

To make this test run you need to specify the correct mock override for getConfiguration. Can you do it?

Let's try

  jest.mocked(getConfiguration).mockReturnValue({
    // 50 different fields
    user:{
     // 20 other fields,
     isAdmin: true
    }
  });
Enter fullscreen mode Exit fullscreen mode

So you can, but you have to specify the "whole world", or typescript will not be happy.

You will not like it as well 🤮

What about defaults?

A better way to manage mocks is by providing defaults one can "extend" from

For example

import {defaultConfiguration} from './configuration-test-utils';
// ...
jest.mocked(getConfiguration).mockReturnValue(
    merge({}, defaultConfiguration, {
       user:{
        isAdmin: true
      }
);
Enter fullscreen mode Exit fullscreen mode

That would greatly simplify life and establish an environment easier to maintain.

However it is still about mocking the whole world, while you need only one field 😭

Only one!

In out case we dont need to mock everything, it's more about TypeScript being too type-safe. Let's stop this!

jest.mocked(getConfiguration).mockReturnValue({
    user:{
     isAdmin: true
    }
} as any/* 😘 */);

// test passes ✅
Enter fullscreen mode Exit fullscreen mode

Well, as any is not the best idea. Here is where DeepPartial helps

Partial is a TypeScript helper making keys of an object non-required. DeepPartial is just a recursive helper for it. It has many implementations, here is one

This gives us the ability to write code like

jest.mocked(getConfiguration).mockReturnValue({
    user:{
     isAdmin: true
    }
} as DeepPartial<ReturnType<typeof getConfiguration>>);

Enter fullscreen mode Exit fullscreen mode

Yeah, the last line is 🤮 and one can improve it with utilities like shoehorn hiding all complexity underneath.

import { fromPartial } from "@total-typescript/shoehorn";
jest.mocked(getConfiguration).mockReturnValue(fromPartial({
    user:{
     isAdmin: true
    }
}));
Enter fullscreen mode Exit fullscreen mode

Better? Better!

....

However, what would happen if we change our code? You know - code always drifting somewhere...

export const isAdmin = () => (
  getConfiguration().user.isAdmin || 
  getConfiguration().user.isSuperAdmin
)
Enter fullscreen mode Exit fullscreen mode

No tests will notice the difference, but our partial mock is no longer the correct one for our use case.

We need something better.

“actually better” is to refactor the code in a way you dont need to mock everything, but we are trying to complete the task without changing the game rules as not everybody can afford refactoring and not everybody want to make their test more testable for the sake or abstract testability (aka test induced design damage)

Something better

Let's change our test a little bit

// index.spec.ts
import {partialMock} from 'partial-mock'; // ⬅️⬅️
import {getConfiguration} from './config';
import {isAdmin} from './';

jest.mock('./config');

test('isAdmin', () => {
  // ⬇️⬇️
  jest.mocked(getConfiguration).mockReturnValue(partialMock({
    user:{
     isAdmin: true
    }
  });

  expect(isAdmin()).toBe(true);
  // but instead it will throw 
});
Enter fullscreen mode Exit fullscreen mode

Here we used partialMock utility to apply DeepPartial(but shoehorn can do it), but also break test because the provided mock no longer represents the case.

Failure example

What about "over mocking"?

Imagine the opposite situation - something complex becomes simpler, but your mocks are still too much.

For example, imagine we do define isSuperAdmin missing in the example above, but we will no longer use it

const mock = partialMock<Data>({
  isAdmin: true,
  isSuperAdmin: true,
});
expectNoUnusedKeys(mock);
Enter fullscreen mode Exit fullscreen mode

Sandbox - https://codesandbox.io/p/sandbox/overmocking-partial-mock-example-z8qd5w?file=%2Fsrc%2Findex.ts%3A22%2C1

unused keys

That's it, folks

That's it - partial mock helps with the "over-mocking", situations where you mock too much, and it also solves problems with the "under-mocking" where you missing some important pieces.

Pretty sure you never though about under-mocking. Until now.

Link to follow:

Top comments (0)