close

prefer-mock-promise-shorthand

Added in v0.9.4

Configuration

rslint.config.ts
import { defineConfig, jestPlugin } from '@rslint/core';

export default defineConfig([
  jestPlugin.configs.recommended,
  {
    rules: {
      'jest/prefer-mock-promise-shorthand': 'error',
    },
  },
]);

Rule Details

Prefer mockResolvedValue() and mockRejectedValue() when a mock is configured to return Promise.resolve() or Promise.reject(), either through mockReturnValue() or through a mockImplementation() callback that does nothing but return the promise. The Once variants map to mockResolvedValueOnce() and mockRejectedValueOnce(). The shorthand builds a new promise on every call; a rejected promise passed to mockReturnValue() is built when the mock is configured, and becomes an unhandled rejection if the mock is never called.

The rule matches on the method name alone, so it applies to any receiver: a mock from jest.fn(), a spy from jest.spyOn(), or a mock held in a variable. Dot, string-literal and template-literal accessors are all recognized, and so is optional chaining. The promise has to come from the global Promise, in any of the same accessor spellings, and may be wrapped in a type assertion; a Promise declared in the file is ignored. It is fixable.

A mockImplementation() callback evaluates the promise's value on every call, while the shorthand evaluates it once, when the mock is configured, so the callback is left alone unless that move is equivalent. That excludes:

  • a callback taking parameters, including a TypeScript this parameter;
  • a generator, and a callback declaring type parameters;
  • a function callback whose value reads this, arguments or new.target, because all three are bound by the call. An arrow callback has none of its own and is treated normally;
  • a block body whose first statement is not a return;
  • a value that writes to something — an assignment, an increment or a delete — or reads a binding declared with let or var, including a read inside a function the value hands back;
  • a value that spreads its arguments, Promise.resolve(...values), because the spread iterates its operand on every call;
  • a value containing an await, which belongs to the async callback it is written in. An async callback is otherwise treated like any other.

None of these exclusions apply to mockReturnValue(), whose argument is already evaluated once.

Examples of incorrect code for this rule:

jest.fn().mockImplementation(() => Promise.resolve(123));
jest
  .spyOn(fs.promises, 'readFile')
  .mockReturnValue(Promise.reject(new Error('oh noes!')));

myFunction
  .mockReturnValueOnce(Promise.resolve(42))
  .mockImplementationOnce(() => Promise.resolve(42))
  .mockReturnValue(Promise.reject(new Error('too many calls!')));

Examples of correct code for this rule:

jest.fn().mockResolvedValue(123);
jest.spyOn(fs.promises, 'readFile').mockRejectedValue(new Error('oh noes!'));

myFunction
  .mockResolvedValueOnce(42)
  .mockResolvedValueOnce(42)
  .mockRejectedValue(new Error('too many calls!'));

let attempt = 0;
jest.spyOn(retry, 'run').mockImplementation(() => Promise.resolve(attempt));

The autofix renames the method and replaces the promise, or the callback returning it, with the value the promise was built from, or with undefined when it was built without one, written void 0 where a local binding named undefined would be read instead. It keeps the accessor's dot, quote or template-literal style, any optional chaining, and any further arguments the call was given. Redundant parentheses around the value are dropped, except around a comma expression, where they decide which operand the promise settles with. No fix is offered when the promise is given more than one argument, when the mock call has type arguments, when a type assertion wraps a resolved promise, or when the rewrite would delete a comment or a declaration the value relies on.

A resolved promise is rewritten only when its value is a primitive literal: a number, string, boolean, null or undefined. Any other value is reported without a fix, because Promise.resolve() checks only the promise it builds against what the mock returns, while mockResolvedValue() checks the value itself against the settled type, so a value that is itself a promise, a generic value or an object literal with an extra property can stop type-checking. A rejected promise is rewritten whatever its reason.

Original Documentation