This introduces an easy-to-use, lightweight and concise way to (partially) mock typed APIs (Typescript modules, types, classes and above all interfaces) without introducing any breaking change to the API.
Mocking interfaces on a per-test basis is not possible right now in jest. IMO it is good testing practices to NOT re-use mocks across tests as this quickly makes the mock become a hard-to-maintain object in its own right. Plus shared mocks introduce unwanted dependencies between tests.
It is rather established practice to generate light-weight throw-away mocks for each test case that only mock a minimal set of API methods to document what is actually being used by the SUT (and throwing errors if something unexpected is being used). This is currently not well supported by jest - neither for modules nor for hashes/classes and not at all for interfaces.
The mocking of interfaces is central to good programming practices, though, as APIs should always be implementations of interfaces and tests should mock the interface rather than a specific implementation as this will much better decouple the test from the underlying API and avoid false negatives when the implementation details change.
// The interface to be mocked - no implementation must be known/available!
// It is even possible to do TDD with deferred implementation of the API, e.g.
// when the API is to be developed at a later time/in parallel by another team
// or team mate.
interface MyApi {
someMethod(x: number): string;
someOtherMethod(): void;
someThirdMethod(): void;
}
test('...', () => {
// Declare the mock - the correct Mocked type will be automatically
// inferred when initialising the variable right away (const ... = mock<...>(...))
// which makes the implementation even more concise.
let myMockedApi: Mocked<MyApi>
// Partially mocking the class or interface. The mock function takes the methods to be
// mocked as arguments. The arguments are strongly typed - so TS will throw a compile-
// time error if you try to mock non-existent methods.
// The test documents which methods are expected to be used by the SUT for the given
// test case.
myMockedApi = mock<MyApi>('someMethod', 'someOtherMethod')
// TS-enabled IDEs will now provide completion for both, the mock functions as well
// as the original function itself!
myMockedApi.someMethod(...) // IDE will propose/check the "x" argument
myMockedApi.someMethod.mockReturnValue('12345') // IDE will propose/check all mock methods
// Matchers work normally:
expect(myMockedApi.someMethod).toHaveBeenCalledWith(...)
// If the SUT invokes a non-mocked (unexpected) method on the API an error will be thrown:
myMockedApi.thirdMethod() // will throw
})
Jest wants to provide a best-in-class typed mocking solution based on current best testing practice and comparable in capability to established typed mocking solutions for other languages. Jest has chosen TypeScript as one of its major language targets and therefore wants to provide best-in-class TypeScript/IDE support for its API. Currently a fundamental mocking feature is missing, though, which often means that users that want full typing of their mocks are forced to use 3rd party mocking solutions or create/maintain their own and cannot use jest's built-in mocking.
Obs: This sample implementation currently requires @types/jest 24.x plus the changes proposed in https://github.com/DefinitelyTyped/DefinitelyTyped/pull/32956 (PR) and https://github.com/DefinitelyTyped/DefinitelyTyped/issues/32901 (Issue). It is however easily adaptable to work with mainline @types/jest or any (future) jest core typings. We chose to base our proposal on patched typings to show how we think this should be done properly (based on our current personal opinions and preferred choices).
type GenericFunction = (...args: any[]) => any
export type MockFunction<F extends GenericFunction> = F & jest.Mock<ReturnType<F>, ArgsType<F>>
export type Mockable<T> = {
[K in keyof T]: GenericFunction
}
export type Mocked<T extends Mockable<T>> = {
[K in keyof T]: MockFunction<T[K]>
}
type PropOf<T> = T[keyof T]
export function mock<T extends Mockable<T>>(...mockedMethods: (keyof T)[]): Mocked<T> {
const mocked: Mocked<T> = {} as Mocked<T>
mockedMethods.forEach(mockedMethod => mocked[mockedMethod] = jest.fn<PropOf<T>>() as MockFunction<PropOf<T>>)
return mocked
}
Thanks for the detailed proposal!
This is related to #4257 - whatever API we come up with should work with both Flow and TS. I don't know enough (about either type system) to really contribute a lot to this conversation, but on the surface something like what you propose sounds awesome.
/cc @orta @aaronabramov @cpojer
@SimenB: I agree! :-) Contrary to #4257 the intention of this feature proposal is not to propose a specific jest mocking API but some practical ideas how to implement strongly typed interface mocking in TypeScript. Very much looking forward to the API you'll come up with in the future.
Once you have decided upon a target API I will most probably be able to adapt this feature request accordingly while maintaining its basic capability. Just let me know.
Thanks for your great testing framework btw. It's a joy to work with. :-)
Here is a more advanced version that allows to mock types with non-function props:
type GenericFunction = (...args: any[]) => any
type PickByTypeKeyFilter<T, C> = {
[K in keyof T]: T[K] extends C ? K : never
}
type KeysByType<T, C> = PickByTypeKeyFilter<T, C>[keyof T]
type ValuesByType<T, C> = {
[K in keyof T]: T[K] extends C ? T[K] : never
}
type PickByType<T, C> = Pick<ValuesByType<T, C>, KeysByType<T, C>>
type MethodsOf<T> = KeysByType<Required<T>, GenericFunction>
type InterfaceOf<T> = PickByType<T, GenericFunction>
type PartiallyMockedInterfaceOf<T> = {
[K in MethodsOf<T>]?: jest.Mock<InterfaceOf<T>[K]>
}
export function mock<T>(...mockedMethods: MethodsOf<T>[]): jest.Mocked<T> {
const partiallyMocked: PartiallyMockedInterfaceOf<T> = {}
mockedMethods.forEach(mockedMethod =>
partiallyMocked[mockedMethod] = jest.fn())
return partiallyMocked as jest.Mocked<T>
}
Is there any indication to when an improvement can be expected? I'm really struggling trying to get my typings correct with a strict tslint setup. I've asked help on discord and create an issue on stackoverflow but I'm hitting a wall.
Looking at this issue this improvement is exactly what I'm looking for!
@jerico-dev This is excellent work. I was just looking for how to do this myself. Thanks for sharing this with the community. It would be great to see this pulled into mainline of Jest typing.
Thanks. It works. This is what I am looking for.
My issue is: I don't want to mock all method for a class, because those methods are not ready to be tested. But I need let the type validation of typescript pass firstly without modifying the interfaces for implementation.
For example, if I don't use the partial mock function, tsc will throw a type error for adSubscriptionDataSource
like this:
is missing the following properties from type 'IAdSubscriptionDataSource': updateById, findByActive, relation, find, and 6 more
some methods of adSubscriptionDataSource
are not ready to be tested, so I don't want to mock them at this time. I just need the type validation of typescript pass. Use mock
helper function can do this and take care of the partial mocked type issue.
This typed way should be documented.
Hey guys,
After reading all of this and a few other articles online, since jest upgrade I'm no longer able to mock a class partially targetting specific method.
Do you guys have a proper way to mock a class? Create an instance right after and injecting it?
Thank you :)
I ended up writing a library to do this - https://github.com/marchaos/jest-mock-extended, which follows @jerico-dev's initial proposal pretty closely, but adds some extra stuff like calledWith which was another use case that we are using.
Most helpful comment
Here is a more advanced version that allows to mock types with non-function props: