refactor(jest-mock)!: simplify usage of jest.fn generic type arguments#12489
refactor(jest-mock)!: simplify usage of jest.fn generic type arguments#12489SimenB merged 24 commits intojestjs:mainfrom mrazauskas:refactor-Mock-generic-type-arguments
jest.fn generic type arguments#12489Conversation
|
Trying out types while looking through examples in docs. It might be good idea to add a tab with TypeScript flavoured examples. Just trying out what is possible and how it works. With current types: const asyncMock = fn<Promise<string>, []>()
.mockResolvedValue('default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call');
const mock = fn<number, []>()
.mockReturnValue(42)
.mockReturnValueOnce(35)
.mockReturnValueOnce(12);With types of this branch (more verbose, but also more clear): const asyncMock = fn<() => Promise<string>>()
.mockResolvedValue('default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call');
const mock = fn<() => number>()
.mockReturnValue(42)
.mockReturnValueOnce(35)
.mockReturnValueOnce(12);Of course, this is only necessary if const asyncMock = fn(async () => 'default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call');
const mock = fn(() => 42)
.mockReturnValueOnce(35)
.mockReturnValueOnce(12); |
|
Just push docs with TS examples. Preview is here – https://deploy-preview-12489--jestjs.netlify.app/docs/next/mock-function-api (Might be there is better way to setup tabs. I have zero experience with Docusaurus, so just followed their docs.) |
| ```bash | ||
| expect(mockedFunction).toHaveBeenCalled() | ||
|
|
||
| Expected mock function "mockedFunction" to have been called, but it was not called. |
There was a problem hiding this comment.
The actual message is (feels to be redundant, or?):
Expected number of calls: >= 1
Received number of calls: 0
There was a problem hiding this comment.
really? that's a regression then. The point is that we print the mockName provided
There was a problem hiding this comment.
The name gets printed. The line expect(mockedFunction).toHaveBeenCalled() is correct, but Expected mock function "mockedFunction" to have been called, but it was not called prints as Expected number of calls... I will double check.
There was a problem hiding this comment.
Aha. Still a regression, we shouldn't be printing numbers when we're expecting any. Makes sense when we expect none, tho
There was a problem hiding this comment.
Alright. Would be better to leave the line in place and to open an issue? So we don’t forget to look at it. Right?
|
Loving the docs changes! 👍 Good idea to use tabs |
| export interface Mock<T, Y extends Array<unknown> = Array<unknown>> | ||
| type UnknownFunction = (...args: Array<unknown>) => unknown; | ||
|
|
||
| export interface Mock<T extends FunctionLike = UnknownFunction> |
There was a problem hiding this comment.
There is a need to keep anys in type FunctionLike = (...args: any) => any. It does not work with unknowns either internally, or something breaks in tests. Despite anys, this is not any (; It is AnyFunction and that’s a constrain. Passing anything what is not a function will throw TS error. To avoid anys leak to user side I used defaults of (...args: Array<unknown>) => unknown. This means that using fn() without any implementation will default to unknown, if implementation is provided types are inferred.
There was a problem hiding this comment.
Trying to explain this in more detail, because I repeated this pattern several times. Internally all what’s necessary is to be sure that we have a function, hence simply any function works. It also works as a constrain. But the default on user side always is (...args: Array<unknown>) => unknown.
There was a problem hiding this comment.
Sounds good 👍 probably worth a code comment?
The credits goes to Playwright – https://playwright.dev/docs/intro#first-test They are doing it in much cleaner way. I couldn’t figure out if they use some plugin or is this some React component? https://github.com/microsoft/playwright/blob/80a38a39c2c66cfb082ad1031dbddd537553380e/docs/src/intro-js.md?plain=1#L52 |
docs/MockFunctionAPI.md
Outdated
| }); | ||
| ``` | ||
|
|
||
| ### `jest.MockedFunction` |
There was a problem hiding this comment.
should we remove this as well? comes from @types/jest and not us
There was a problem hiding this comment.
Right. I took it out and will bring it back after jest.Mocked refactor. What about that note on Create React App? Just above?
There was a problem hiding this comment.
yeah, kill it. only thing under the TypeScript heading should be either pointing to "getting started" or examples using jest from @jest/globals. All the other stuff (CRA, "see this repo", "jest is written in TS") should go, IMO
| expect(mockAdd).toBeCalledWith(1, 2); | ||
| }); | ||
| ``` | ||
| ### `jest.fn(implementation?)` |
There was a problem hiding this comment.
By the way, I double checked if the example works. All is correct. Will add it to examples folder later. TypeScript example will need more changes.
There was a problem hiding this comment.
convert back to draft so I don't accidentally merge? 😀
Co-authored-by: Simen Bekkhus <sbekkhus91@gmail.com>
SimenB
left a comment
There was a problem hiding this comment.
this is fantastic stuff @mrazauskas! Thank you so much for diving in and fixing this 👍
docs/MockFunctionAPI.md
Outdated
| _Note: `jest.fn(implementation)` is a shorthand for `jest.fn().mockImplementation(implementation)`._ | ||
|
|
||
| For example: | ||
| Both `jest.fn(fn)` and `jest.fn().mockImplementation(fn)` are equivalent. For instance, you can use `.mockImplementation()` to replace the implementation of a mock: |
There was a problem hiding this comment.
stick in :::tip? or note I guess
There was a problem hiding this comment.
The idea was to refactor this note into a sentence, which should sell .mockImplementation(). If both are equivalent, why choose the longer one? For instance, .mockResolveValue() or .mockRejectedValue() have similar sentence followed by example. That’s why I tweak the following example too. Does not work? Hm.. Read it all again. I deleted too much. Will fix it.
There was a problem hiding this comment.
I think the example change is good since it's shorter, but I think highlighting the fact the two approaches are the same is a good idea 🙂
There was a problem hiding this comment.
Right. I reverted most of text. It sounds good.
It was fun to dive in. Also I am happy with the result. Internals are cleaner, usage is simplified and we have a few more type tests too ;D |
|
When should we expect this in a release? |
|
I can make one tomorrow (bedtime here now) |
|
Perfect! Haha sorry, not trying to rush you by any means, was just curious 😄 |
|
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Closes #12479
Summary
jest.fnandjest.spyOndepend onMockandMockInstancetypes. These take two generic type arguments: the returned value, parameters type. That works just fine internally, but makes usage hard for on the user side. For instance, here is a line from Jest docs:All looks fine at first glance, but the thing is that type returned by
jest.fnisMockand it is slightly different fromMockedFunction. As it was pointed out in the link issue, to have the right type currently user should do something like this:This PR makes it possible to type
jest.fnin much more simple way (which is also more precise than type casting usingMockedFunction):Looks like I made it work as expected. Have to take a second look tomorrow at a couple of
anys here and there. Seems like internally they are necessary. On user side, everything should default tounknown.Test plan
It was priceless to have type tests in place! All should pass.