Proxy-base, testing-framework-independent solution to solve overmocking, and undermocking. Never provide more information than you should, never provide less.
- solves the
as
problem in TypeScript tests when an inappropriate object can be used as a mock - ensures provided mocks are suitable for the test case
npm add --dev partial-mock
Problem statement
type User = {
id: string;
name: string;
// Imagine oodles of other properties...
};
const getUserId = (user:User) => user.id;
it("Should return an id", () => {
getUserId({
id: "123", // I really dont need anything more
} as User /* 💩 */);
});
// ------
// solution 1 - correct your CODE
const getUserId = (user:Pick<User,'id'>) => user.id;
getUserId({
id: "123", // nothing else is required
});
// solution 2 - correct your TEST
it("Should return an id", () => {
getUserId(partialMock({
id: "123", // it's ok to provide "partial data" now
}));
});
// Example was adopted from `mock-utils`
But what will happen with solution 2 in time, when internal implementation of getUserId change?
const getUserId = (user:User) => user.uid ? user.uid : user.id;
This is where partial-mock
will save the day as it will break your test
🤯Error: reading partial key .uid not defined in mock
import {partialMock} from 'partial-mock';
// complexFunction = () => ({
// complexPart: any,
// simplePart: boolean,
// rest: number
// });
jest.mocked(complexFunction).mockReturnValue(
partialMock({simpleResult: true, rest: 1})
);
// as usual
complexFunction().simpleResult // ✅=== true
// safety for undermocking
complexFunction().complexPart // 🤯 run time exception - field is not defined
import {partialMock, expectNoUnusedKeys} from 'partial-mock';
// safety for overmocking
expectNoUnusedKeys(complexFunction()) // 🤯 run time exception - rest is not used
API
-
partialMock(mock) -> mockObject
- to create partial mock (TS + runtime) -
exactMock(mock) -> mockObject
- to create monitored mock (runtime) -
expectNoUnusedKeys(mockObject)
- to control overmocking -
getKeysUsedInMock(mockObject)
,resetMockUsage(mockObject)
- to better understand usage -
DOES_NOT_MATTER
,DO_NOT_USE
,DO_NOT_CALL
- magic symbols, see below
Theory
Definition of overmocking
Overtesting is a symptom of tests doing more than they should and thus brittle. A good example here is Snapshots capturing a lot of unrelated details, while you might want to focus on something particular. The makes tests more sensitive and brittle.
Overmocking
is the same - you might need to create and maintain complex mock, while in fact a little part of it is used.
This makes tests more complicated and more expensive to write for no reason.
(Deep) Partial Mocking for the rescue! 🥳
Example:
const complexFunction = () => ({
doA():ComplexObject,
doB():ComplexObject,
});
// direct mock
const complexFunctionMock = () => ({
doA():ComplexObject,
doB():ComplexObject,
});
// partial mock
const complexFunctionPartialMock = () => ({
doA():{ singleField: boolean },
});
And there are many usecases when such mocks are more than helpful, until they cause Undermocking
Definition of undermocking
Right after doing over-specification, one can easily experience under-specification - too narrow mocks altering testing behavior without anyone noticing.
⚠️ partial-mock will throw an exception if code is trying to access not provided functionality
A little safety net securing correct behavior.
If you dont want to provide any value - use can use DOES_NOT_MATTER
magic symbol.
import {partialMock, DOES_NOT_MATTER} from "partial-mock";
const mock = partialMock({
x: 1,
then: DOES_NOT_MATTER
});
Promise.resolve(mock);
// promise resolve will try to read mock.then per specification
// but it "DOES_NOT_MATTER"
DOES_NOT_MATTER
is one of magic symbols:
-
DOES_NOT_MATTER
- defines a key without a value. It is just more semantic than setting key toundefined
. -
DO_NOT_USE
- throws on field access. Handy to create a "trap" and ensure expected behavior. Dont forget that partial-mock will throw in any case on undefined field access. -
DO_NOT_CALL
- throws on method invocation. Useful when you need to allow method access, but not usage.
Non partial mocks
Partial mocks are mostly TypeScript facing feature. The rest is a proxy-powered javascript runtime. And that runtime, especially with magic symbol defined above, can be useful for other cases.
For situation like this use exactMock
Inspiration
This library is a mix of ideas from:
- react-magnetic-di - mocking solution with built-in partial support. Partial-mock is reimplementation of their approach for general mocking.
- mock-utils - typescript solution for partial mocks. Partial-mocks implements the same idea but adds runtime logic for over and under mocking protection.
- rewiremock - dependnecy mocking solution with over/under mocking protection (isolation/reverse isolation)
- proxyequal - proxy based usage tracking
Licence
MIT