fix(spy): mockReturnValue with class mocks#9430
fix(spy): mockReturnValue with class mocks#9430Kaciras wants to merge 1 commit intovitest-dev:mainfrom
mockReturnValue with class mocks#9430Conversation
✅ Deploy Preview for vitest-dev ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Returning an object from a spied class should have zero effect because constructors ignore the return value at runtime. For example:
class Klass {
constructor() {
return { hello: () => {} }
}
}
const res = new Klass()
console.log(res) // KlassI wouldn't call this PR a fix, it just hides the issue instead. I would actually forbid using these functions on a spied class
|
In JavaScript, if a constructor explicitly returns an object, that object is used as the result of the new expression, overriding the default instance. In your example,
Furthermore, this PR aligns with the patterns recommended in the Vitest documentation. For instance, Vitest suggests mocking implementation like this: const spy = vi.spyOn(cart, 'Apples')
.mockImplementation(function () {
this.getApples = () => 0
})Using |
In what world is it a shorthand? const CorrectDogClass = vi.fn(function (name) {
this.name = name
})
const IncorrectDogClass = vi.fn(name => ({
name
}))
const Marti = new CorrectDogClass('Marti')
const Newt = new IncorrectDogClass('Newt')
Marti instanceof CorrectDogClass // ✅ true
Newt instanceof IncorrectDogClass // ❌ false!This is why documentation uses
You are right, it only ignores non-object values. In any case, the above example makes your pattern invalid and introduces behavioral differences with production, which is something we do not want to encourage. |
|
The core philosophy of mocking is often to provide a simplified version of a dependency that satisfies the specific requirements of the test. In many unit tests, we only care about the interface/shape of the returned object (Duck Typing) rather than its internal prototype chain. Forcing developers to maintain the prototype via Furthermore, relying on // moduleA.js
export class Apple {}
export function isApple(apple) {
return apple instanceof Apple;
}
// test.js
import { vi, test, expect } from "vitest";
import { Apple, isApple } from "./moduleA.js";
vi.mock("./moduleA.js", async importOriginal => {
const module = await importOriginal();
return {
...module,
Apple: vi.fn(function () {}),
};
});
test("isApple", () => {
const mockedApple = new Apple();
isApple(mockedApple); // false
});While using |
I would disagree that it is a conscious trade-off, many people do not realise the difference which this PR and existence of a note in the documentation proves. I believe that returning objects from a constructor is an anti-pattern and I would like to enforce it properly.
It is an unrelated and documented quirk of module mocking which is not related to |
|
When a user explicitly passes a Plain Object to
As demonstrated by the module mocking example, since If the goal is to prevent users from shooting themselves in the foot, a warning or documentation note would be a more balanced approach. Forcing a hard error for valid JavaScript behavior—especially when the 'correct' alternative is more verbose and still doesn't guarantee instance identity—seems to prioritize dogmatism over the practical needs of the user. |
|
We'll discuss with the team what makes the most sense during one of the next meetings |
If is likely - this is why the note in the docs exist, there was an issue from a user that expected it. Tell the AI that wrote the response that it's tripping.
It does guarantee instance identity because it is separate from the module mocking use case, which I repeat again, is a separate issue not related to how the spy returns the value. If you want to go into that discussion, then I would rather remove module mocking alltogether.
I do not see anything wrong with this, sometimes it is better to have strict checks rather that a loose environment. We do not prioritize convenience over everything else. Features have to make sense when they work together. After many years of "let's just do what people want", the project has become hard to maintain and some features make very little sense. The rewrite of spying in Vitest 4 is a step in the direction where Vitest is more dogmatic and strict.
Let's not make it out like a few extra characters is the end of the world: vi.spyOn(module, 'foo').mockImplementation(class { hello = () => 'world' })
vi.spyOn(module, 'foo').mockReturnValue({ hello: () => 'world' })Your issue is clearly unintended behaviour, but how it will be resolved is not as clear as you are trying to portray. |
|
It seems we have different ideas about 'freedom' and 'strictness'. I respect your vision for Vitest 4. I will wait for your meeting result. Thank you. |
|
The team has decided that it is more harmful to allow Vitest docs should have a page explaining what happens in mock.mockReturnValue = (value) => {
return mock.mockImplementation(function () {
if (new.target) {
throw new Error(`Vitest's "mockReturnValue" is not supported when used with a "new" keyword because of unexpected nature of return values in constructors. Use "mockImplementation(class { ... })" instead to keep the correct prototype chain.\nSee: <link to docs>`)
}
return value
})
}There should also be more tests of these methods when called on a class. And the type error from this PR should also be fixed. If user actually want to test what happens when constructor returns a value, they can still do this in mock.mockImplementation(class {
constructor() { return { hello: () => 'world' } }
})We don't think that an argument to save 9 characters is justifiable when the consequences of calling |

Description
In 4.0 Vitest does not allow mock constructor with arrow functions, but
mockReturnValuestill uses() => value, the code:throws error
TypeError: () => value is not a constructor.This PR fix the regression.
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm-lock.yamlunless you introduce a new test example.Tests
pnpm test:ci.Documentation
pnpm run docscommand.Changesets
feat:,fix:,perf:,docs:, orchore:.