-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce a more flexible and better typed way to mock APIs #7832
Comments
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. |
@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
some methods of 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. |
This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 14 days. |
This issue is not stale, at least not for me. If it wasn't for @marchaos library (thanks a lot!), I'd consider dropping jest in favor of other testing/mocking frameworks like mocha. This is an essential feature, and a much better approach to writing tests in general IMHO. |
import {jest} from '@jest/globals';
interface MyApi {
someMethod(x: number): string;
someOtherMethod(): void;
someThirdMethod(): void;
}
const mockedApi = jest.mocked({} as MyApi); So I think this issue as stated in the OP is solved? Note that this will require you to have a mock object. Main difference is we need to get passed an object with mocks. We could probably add a import * as someModule from 'some-module';
import {jest} from '@jest/globals';
jest.mock('some-module');
const mockedModule = jest.mocked(someModule); Is better than some proxy as your setting up mocks that will be used by others. Could you show real use cases where the proxy approach is better/cleaner? The other feature |
@SimenB I'm not too familiar with the differences between My main gripe with Jest's mocking is the reliance on global/static mocking (of modules). I highly prefer building my code with classes that their dependencies are injected via their constructor; in testing, that means injecting local mock instances instead of mocking modules globally in place. I completely understand the reasoning behind Jest's approach given that many JS codebases are still mainly procedural and don't use the dependency-injection approach of OOP. I hope Jest could also provide the alternative for OOP ehnthusiastics :) |
You can also do One limitation of the approach from the OP (and I guess That said, I do think something like import {jest} from '@jest/globals';
interface MyApi {
someMethod(x: number): string;
someOtherMethod(): void;
someThirdMethod(): void;
}
const myApi: MyApi = {
someMethod: jest.fn<(x: number) => string>(),
someOtherMethod: jest.fn(),
someThirdMethod: jest.fn(),
};
const mockedApi = jest.mocked(myApi); is fine. The Slightly less typing import {jest} from '@jest/globals';
interface MyApi {
someMethod(x: number): string;
someOtherMethod(): void;
someThirdMethod(): void;
}
const mockedApi = jest.mocked({
someMethod: jest.fn(),
someOtherMethod: jest.fn(),
someThirdMethod: jest.fn(),
} as MyApi); If you have a "real" implementation you want to create a mock from, we could probably expose what |
Other types such as? you mean plain properties?
I would love to not need to explicitly set each function as mock, i.e: const mockedApi = jest.mocked<MyApi>(); or something along these lines. This is similar to the approach taken by Sinon's createStubInstance and Java's mocking libraries such as Mockito. |
If you by "plain properties" mean primitives, then yes. And classes or arrays. We also detect generator functions or async functions and return the correct type of function. Both sinon and mockito seems to require passing an argument, which is what I'm suggesting? While I've used both in the past, it's been more than 6 years since either, so I've forgotten what I once knew about them 🙈 |
Just a quick note. At the moment I am reworking import myApi from './myApi';
jest.mock('./myApi');
const mockedApi = myApi as jest.Mocked<typeof myApi>; |
I think it's sufficient to only mock functions (including getters/setters).
When stubbing a specific function? yes, but not when initializing the mock/stub. |
This was already possible in jasmine for ages with |
Any update on this @SimenB ? Our teams are forcibly relying on external mocking libraries such as Sinon.js, moq.ts, td.js just for this feature. It is regrettable since jest already has all the foundation for creating mocks plus custom matchers. |
This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days. |
Not stale. |
🚀 Feature Proposal
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.
Motivation
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.
Example
Pitch
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.
Working sample implementation of the above mocking API
Obs: This sample implementation currently requires @types/jest 24.x plus the changes proposed in DefinitelyTyped/DefinitelyTyped#32956 (PR) and DefinitelyTyped/DefinitelyTyped#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).
The text was updated successfully, but these errors were encountered: