eslint-plugin-jest icon indicating copy to clipboard operation
eslint-plugin-jest copied to clipboard

Prefer jest.MockedFunction<T> over jest.Mock

Open entropitor opened this issue 5 years ago • 16 comments

In typescript

const fnMock = fn as jest.Mock;

is less ideal then

const fnMock = fn as jest.MockedFunction<
  typeof fn
>;

As the latter forces us to implement the mock correctly (have the right types), would a lint rule enforcing this behavior fall under the scope of this plugin?

entropitor avatar Nov 27 '20 09:11 entropitor

I'm not sure if we should get involved with recommendations for @types/jest. Very much open to being convinced otherwise, tho.

@G-Rath @jeysal @thymikee thoughts?

SimenB avatar Nov 27 '20 10:11 SimenB

I'd be more ok with it if the type was coming from Jest itself. Will pass on this one, won't pursue it, won't block it.

thymikee avatar Nov 27 '20 10:11 thymikee

I'd be more ok with it if the type was coming from Jest itself.

Definitely. That's still some time out (unless someone else wants to pursue it), so might be worth doing something else in the meantime? Not sure

SimenB avatar Nov 27 '20 10:11 SimenB

This would be the first typescript-eslint rule wouldn't it? I suppose it would need some other infra, a new preset to opt into or something?

jeysal avatar Nov 27 '20 12:11 jeysal

Don't think so, it'd just look for some other nodes - if they're not there the rule does essentially nothing?

SimenB avatar Nov 27 '20 12:11 SimenB

I guess in this case yeah, but some other TS-related rules may only be a good idea to activate when actually using TS, e.g. some sort of "force X to have a type declared", hence perhaps a ESLint-plugin/preset-jest-typescript may make sense

jeysal avatar Nov 27 '20 12:11 jeysal

I've thought about this in the past - I'm not against TS-related rules, but I couldn't come up with any that actually overlapped with jest that would be relevant to enough of the community to be worth shipping here.

Doing suggestions on types from @types/jest seemed like an obvious one, but the value didn't win out vs the effort in both implementation and maintenance vs. using @typescript-eslint/ban-types which is meant for exactly this sort of situation.

The math was around the range of types we could actually make recommendations for (as most of the types in @types/jest have at least one place where they're valid to use, which we couldn't test for), the quality of those recommendations (for similar reasons - types in test-land tend to become very nuanced, especially when using as), and accounting for the versioning of @types/jest, since everything is pretty much always added in a patch release so it'd be easy for a bunch of changes to be landed in a small space of time that makes our rule(s) some degree of wrong (this should be less of a problem in practice, as the types are pretty stable and you should always be able to update to the last, but it's still something that makes me a bit weary).

G-Rath avatar Nov 28 '20 23:11 G-Rath

Going to close this due to lack of activity

G-Rath avatar Oct 09 '21 20:10 G-Rath

I would like to have such a rule as it might help against problems like the one described here: https://www.salto.io/blog-posts/typescript-unit-testing-pitfalls-with-jest-and-how-to-work-around-them#toc-type-safety-for-mocks

chtpl avatar Mar 02 '23 10:03 chtpl

Since Jest now ships its own types, I think adding this rule is fine.

It should target types from @jest/globals tho, not @types/jest. If they overlap, great!

SimenB avatar Mar 02 '23 10:03 SimenB

Would you be able to provide a PR @chtpl?

SimenB avatar Mar 02 '23 10:03 SimenB

The article is talking about @types/jest. I was scanning through it quickly. If I got it right, the problem is that by default jest.Mock returns (...args: Array<any>) => any. The default of jest.Mock which ships with @jest/globals is different: (...args: Array<unknown>) => unknown.

Other difference is that jest.Mock from @types/jest is complicated, because it requires passing the return and arguments type of a function. Usage of jest.MockedFunction is simply: jest.MockedFunction<() => string>. In @jest/globals both of them take just one type argument. Both are simple to use.

The difference between jest.Mock and jest.MockedFunction is very subtile (see documentation). I would recommended using jest.Mock to type the return type of jest.fn() (this is the case from the article).

jest.Mocked (and its constrained sisters: jest.MockedClass, jest.MockedFunction or jest.MockedObject) are meant to describe object (or member of that object) which is the result of calling jest.mock('some-module'). Importing from the mocked module, one will see that typings of mocks will be missing. These types are here to solve the issue.


What might be useful is a rule forcing to pass an argument to all of the utility types. So that the resulting type wouldn’t be based on unknown (that’s always the default), but on some concrete implementation. Here is are the types I have in mind:

  • jest.Mock
  • jest.Mocked, jest.MockedClass, jest.MockedFunction, jest.MockedObject
  • jest.Replaced
  • jest.Spied, jest.SpiedClass, jest.SpiedFunction, jest.SpiedGetter, jest.SpiedSetter

Most of the helper types were recently added. The issue is from the times when @types/jest had just two mock related types. jest.Mock was cumbersome to use because of multiple type arguments. At that time it made sense to recommend using jest.MockedFunction instead.

I was helping to add and polish these helper types to Jest repo (or @jest/globals). Also implemented them in @types/jest (as much as it was possible).

mrazauskas avatar Mar 02 '23 11:03 mrazauskas

@mrazauskas Thanks for the clarification. As I my journy in the JS/TS ecosystem just started, I don't understand what the difference between types/jest and jest/globals is and when I should use which of these two packages

chtpl avatar Mar 02 '23 14:03 chtpl

See https://jestjs.io/docs/getting-started#type-definitions

For a new project I would go for import {describe, expect, test} from '@jest/globals';, but that is just an opinion.

mrazauskas avatar Mar 02 '23 18:03 mrazauskas

OK that clears it up a bit - so types/jest is a bit less verbose because I do not need to import describe, it, expect etc. But why does jest/globals not define those functions the same way so that I wouldn't have to import them explicitly?

chtpl avatar Mar 02 '23 19:03 chtpl

Importing from @jest/globals is the same as importing any other library. Install it and import the APIs. (By the way, any import is somewhat verbose. Why should I import what I just installed? JavaScript does not get that. Well.. TypeScript is build around the same idea.)

The @types/* libraries are special for TypeScript. They are allowed to declare typings of globals like process or window. So @types/jest goes this way and declares globals which Jest injects. For reference see https://www.typescriptlang.org/tsconfig#types

mrazauskas avatar Mar 02 '23 20:03 mrazauskas