esbuild-jest
esbuild-jest copied to clipboard
spyOn: Cannot redefine property
While mocks are working great for me, I'm having some trouble with spies.
I created this code.
import { moduleDependency } from "./dependency";
export const internalDependency = () => "concrete";
export interface Result {
internal: string;
module: string;
}
export const subjectUnderTest = (): Result => {
return {
internal: internalDependency(),
module: moduleDependency(),
};
};
And then a test to exercise it:
import { subjectUnderTest } from ".";
import * as index from ".";
jest.spyOn(index, "internalDependency").mockReturnValue("mocked");
describe("index.ts", () => {
it("can replace internal dependencies with spies", () => {
const result = subjectUnderTest();
expect(result).toEqual({
internal: "mocked",
module: "concrete",
});
});
});
However, running the tests produces an error:
$ /Users/adrian/github.com/a-h/esbuild-mock-test % npx jest
PASS ./index-withmock.test.ts
PASS ./index-withoutmock.test.ts
FAIL ./index-withspy.test.ts
● Test suite failed to run
TypeError: Cannot redefine property: internalDependency
at Function.defineProperty (<anonymous>)
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:826:16)
at Object.<anonymous> (index-withspy.test.ts:21:6)
Test Suites: 1 failed, 2 passed, 3 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.423 s
Ran all test suites.
Am I missing something?
A minimal reproduction is available at https://github.com/a-h/esbuild-mock-test
Versions etc.
"devDependencies": {
"@types/jest": "^26.0.20",
"esbuild": "^0.9.3",
"esbuild-jest": "^0.5.0",
"jest": "^26.6.3",
"typescript": "^4.2.3"
},
+1 on this particular issue; this is the only thing stopping me migrating a particularly chonky codebase from using ts-jest to esbuild-jest (which would take literal minutes off the test run times).
Happy to provide any additional info, or test potential fixes.
@a-h maybe you can check this https://github.com/evanw/esbuild/issues/412 after checking on it, esbuild for now doesnt support esm for jest.spy on
also this https://github.com/evanw/jest-esbuild-demo
Thanks for that. I've refactored the code in the example repo to support spies by moving to the commonjs
format at https://github.com/a-h/esbuild-mock-test/commit/07131865d6f2bc1fc845f9c8f71b5018c3b2f30d
However, if I run the tsc
compiler on the import
/ export const
version, I get a JavaScript test that I can run with npx jest *.js
that does work with spies, so I thought it might be possible somehow.
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
}
}
TypeScript
import * as index from ".";
//@ts-ignore
jest.spyOn(index, "internalDependency").mockReturnValue("mocked");
describe("index.ts", () => {
it("can replace internal dependencies with spies", () => {
const result = index.subjectUnderTest();
expect(result).toEqual({
internal: "mocked",
module: "concrete",
});
});
});
tsc
output
"use strict";
exports.__esModule = true;
var index = require(".");
jest.spyOn(index, "internalDependency").mockReturnValue("mocked");
describe("index.ts", function () {
it("can replace internal dependencies with spies", function () {
var result = index.subjectUnderTest();
expect(result).toEqual({
internal: "mocked",
module: "concrete"
});
});
});
//# sourceMappingURL=index-withspy.test.js.map
I just faced this issue and I managed to workaround it by creating a variable with the methods I want to spy
This doesn't work:
import * as niceGrpc from "nice-grpc";
jest.spyOn(niceGrpc, "createClient").mockReturnValue({});
jest.spyOn(niceGrpc, "createChannel").mockReturnValue({});
This works:
import { createChannel, createClient } from "nice-grpc";
const foo = { createChannel, createClient };
jest.spyOn(foo, "createClient").mockReturnValue({});
jest.spyOn(foo, "createChannel").mockReturnValue({});
I'm trying to use this method with brcypt
but didn't have success.
import { compareSync } from 'bcrypt'
const bcrypt = { compareSync }
jest.spyOn(bcrypt, "compareSync").mockReturnValue(true);
Still returns false
.. any idea what might be happening here?
Just sharing what worked for me. To get around it, I had to create my own bcrypt
namespace like so:
import originalBcrypt from "bcrypt";
export namespace bcrypt {
export const compareSync = originalBcrypt.compareSync;
export const compare = originalBcrypt.compare;
}
and then mocked like so:
import { bcrypt } from "../../src/types"
jest.spyOn(bcrypt, "compareSync").mockReturnValue(true);
@kevindra it worked for me
Any update on this issue, above workaround seems to be not handling edge case.
x method used in two files for making HTTP request. Spying x method using above workaround fixes type error
Pending Issue: Above workaround do not correctly spy x method, may be due to pointing at incorrect location in the memory
unfortunately mockImplementation on above spy still do not work, can someone guide me around same
@akshayr20 - I think you're more likely to get an answer if you provide a reproduction of the issue - e.g. a minimal reproduction of the issue. I don't think your comment is clear enough on what you're asking for help with.
Hi @a-h thanks for replying, I tried reproducing the error in below sample repository
Kindly check the test suite for Desired Result, I have added comment on the motivation behind same: https://github.com/akshayr20/esbuild-jest-bug If someone could help me fix it, it would be really helpful
Any update on above issue, or any hint on where should I look into to debug this further?
Any update on above issue, or any hint on where should I look into to debug this further?
I think another workaround is using jest.mock
like so:
jest.mock('react-i18next', () => ({
useTranslation: jest.fn()
}));
import { useTranslation } from 'react-i18next';
// some test code below
expect(useTranslation).toHaveBeenCalledWith(...);
This is an example with a different library, but should be the same idea.
Hey thanks @aecorredor I handled my situation using this approach https://github.com/evanw/esbuild/issues/412#issuecomment-938583697
dir/index.ts
export * from "./file"
dir/file.ts
export foo = () => {}
file.test.ts
import * as Index from "./dir";
import * as File from "./dir/file";
it ("does not work", () => {
jest.spyOn(Index, "foo")
})
it("works", () => {
jest.spyOn(File, "foo")
})
🤷♀️
the solution of @aecorredor works if you need mock all module but if you need only one function this worked for me:
import * as Foo from 'foo';
jest.mock('foo', () => ({
__esModule: true,
// @ts-ignore
...jest.requireActual('foo'),
}));
it('foo', () => {
jest.spyOn(Foo, 'fooFunction')
})
jest.mock('fooModule', () => {
const originalModule = jest.requireActual('fooModule');
return {
...originalModule,
doThing: jest.fn().mockReturnValue('works!'),
};
});
The root cause is explained in this SO answer, which explains why these workaround works
import { createChannel, createClient } from "nice-grpc";
const foo = { createChannel, createClient };
jest.spyOn(foo, "createClient").mockReturnValue({});
jest.spyOn(foo, "createChannel").mockReturnValue({});
[X] export * from "./file"
[O] export foo = () => {}
I have no clear way to "fix" this, just sharing the information here.
Also it looks like the issue is related to TS https://github.com/microsoft/TypeScript/issues/43081, probably will be fixed in TS 4.5.0
When this problem occurs with external libraries, you can create and use a passthrough function in your own code, which can then be spied on without problems
assignGroupId.ts
import { assignGroupID } from "algosdk";
// This pass through function is to allow spying in the tests
// Without it get a "Cannot redefine property" error
export default assignGroupID;
someSrcFile.ts
import { /* otherFunctionsFromLib */ } from "algosdk";
import assignGroupId from "src/group/assignGroupId";
...
assignGroupId(txns);
someSrcFile.test.ts
import * as assignGroupId from "src/group/assignGroupId";
...
jest.spyOn(assignGroupId, "default").mockImplementation(() => {});
@EwertonLuan this is the best answer for me, not perfect but the cleanest from all, thanks
dir/index.ts
export * from "./file"
dir/file.ts
export foo = () => {}
file.test.ts
import * as Index from "./dir"; import * as File from "./dir/file"; it ("does not work", () => { jest.spyOn(Index, "foo") }) it("works", () => { jest.spyOn(File, "foo") })
🤷♀️
Have you been able to find a way to create the mock the import * as Index from "./dir";
import?
For me, this blog article worked to allow me to mock individual functions of a module for each test. Relevant code below:
import { functionToMock } from "@module/api"; // Step 3.
// Step 1.
jest.mock("@module/api", () => {
const original = jest.requireActual("@module/api"); // Step 2.
return {
...original,
functionToMock: jest.fn()
};
});
// Step 4. Inside of your test suite:
functionToMock.mockImplementation(() => ({ mockedValue: 2 }));
dir/index.ts
export * from "./file"
dir/file.ts
export foo = () => {}
file.test.ts
import * as Index from "./dir"; import * as File from "./dir/file"; it ("does not work", () => { jest.spyOn(Index, "foo") }) it("works", () => { jest.spyOn(File, "foo") })
🤷♀️
dir/index.ts
export * from "./file"
dir/file.ts
export foo = () => {}
file.test.ts
import * as Index from "./dir"; import * as File from "./dir/file"; it ("does not work", () => { jest.spyOn(Index, "foo") }) it("works", () => { jest.spyOn(File, "foo") })
🤷♀️
that works for me! thx
Hey y'all!! This is still an issue as of today, I'm stuck with ts-jest (which does not have the problem) because of this. I have a simple setup file for jest doing this before the tests run:
import * as example from './example'
jest.spyOn(example, 'foo').mockResolvedValue()
and the issue comes up. Do we know why this happens with this package and not ts-jest? Trying to understand the issue better to see if I could try to contribute a fix for it since this is keeping us from using it. Thanks for your amazing work on this!
I have a similar issue but a bit different (ununderstandable solution) :
I have an utils file :
const funcB = ()=>{
...
}
const funcA = ()=>{
x = funcB();
...
}
And a component :
const Component = (props)=>{
const handleClick = () => funcA():
return <Button onClick={handleClick}>click</Button>
}
Before a few days, my spyOn was ok in the test :
import * as Utils from '~/src/common/utils';
...
test(()=>{
const spyOnA = jest.spyOn(Utils, 'funcA');
...
}
But now I have the same error : "cannot redefine property"
My solution is to specify that :
import * as Utils from '~/src/common/utils';
jest.mock('~/src/common/utils', () => ({
...jest.requireActual('~/src/common/utils'),
funcA: jest.fn(),
}));
...
test(()=>{
const spyOnA = jest.spyOn(Utils, 'funcA');
...
}
With that, I suppose we say to use the actual implemantation of funcB 🤷🏻
My solution was to use the construction like
jest.mock('@/shared/lib/notify');
import { notify } from '@/shared/lib/notify';
// ...
it('...', () => {
// ...
if (jest.isMockFunction(notify.openError)) {
expect(notify.openError).toHaveBeenCalledWith({
text: `Error: ${errorText}`,
});
}
jest.restoreAllMocks();
})
This also works if you need all methods:
import * as fooMethods from "nice-grpc";
const foo = {...fooMethods};
jest.spyOn(foo, "createClient").mockReturnValue({});
jest.spyOn(foo, "createChannel").mockReturnValue({});
That was good for us:
///////// above the test content
import { someFunction } from 'some-module';
jest.mock('some-module', () => {
const originalModule = jest.requireActual('some-module');
return {
...originalModule,
someFunction: jest.fn((...args) => originalModule.someFunction(...args)),
};
});
//////////
//////// Inside the test:
expect(someFunction).toHaveBeenCalledWith({ ... });
Hey all, i took a long time to comb through the code and figure out why this is happening. If you are using next/jest
then this is happening because next default the transform
object in your jest config to be swc. In order to fixed this issue all you need to do is override the transform by adding this to you jest config
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
module.exports = createJestConfig({
// other config settings
transform: {
'^.+\\.(js|jsx|ts|tsx|mjs)$': 'babel-jest',
},
});
As long as the transformer your using doesn't cause exports to have un configurable properties you should be just fine
@CalebJamesStevens but the swc/jest is much faster than babel, right? if we want to use swc instead of babel-jest, how to fix the problem?
+1 here, this can be a point where ESBuild can excel over SWC. The SWC team don't seem to care that this is how Jest and its spies have worked for years, and thousands of tests and lines of test code rely on it. It's a testing environment, so making these configurable to match years of expected behavior seems uncontroversial to me; leave the default as is, but allow us to rewrite these as configurable so our tests still pass.
Their take is some version of "it wasn't correct, so we shouldn't fix it," and instead tell us to rewrite all of our tests. Unfortunately, that's not an option for us.

I tryed all the combination proposed above but still not able to pass the test. NB: functions are simplified
services.js
export const getOutputs = (chain) => { return chain };
cli.js
import { getOutputs} from "./services.js";
export function askAndGet(rl_instance, callback) {
rl_instance.question("Enter the chain: ", async (chain) => {
await callback(chain);
rl_instance.close();
});
}
export const followupQuestion = (rl, answer) => {
switch (answer) {
case "outputs":
askAndGet(rl, getOutputs);
break;
default:
console.log("invalid input");
}
};
cli.test.js
import { jest } from "@jest/globals";
import { followupQuestion } from "./cli.js";
import { getOutputs } from "./services.js";
jest.mock("./services.js", () => {
const original = jest.requireActual("./services.js"); // Step 2.
return {
...original,
getOutputs: jest.fn((...args) => original.getOutputs(...args)),
};
});
const readlineMock = {
createInterface: jest.fn().mockReturnValue({
question: jest.fn().mockImplementationOnce((_questionTest, cb) => {
cb("y");
readlineMock.createInterface().close();
}),
close: jest.fn().mockImplementationOnce(() => undefined),
}),
};
describe("followupQuestion", () => {
test("should call getOutputs when the parameter is output", async () => {
const rl = readlineMock.createInterface();
followupQuestion(rl, "outputs");
expect(getOutputs).toHaveBeenCalled();
});
});
with this test the error I get is
Matcher error: received value must be a mock or spy function
Any help?
This fixed a bunch of tests for me https://github.com/aelbore/esbuild-jest/issues/26#issuecomment-1341765767
All I was missing in my transform was mjs
, which I don't use directly in my project.