esbuild-jest icon indicating copy to clipboard operation
esbuild-jest copied to clipboard

spyOn: Cannot redefine property

Open a-h opened this issue 3 years ago • 37 comments

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"
  },

a-h avatar Mar 17 '21 11:03 a-h

+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.

jimmed avatar Mar 18 '21 13:03 jimmed

@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

aelbore avatar Mar 19 '21 06:03 aelbore

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

a-h avatar Mar 19 '21 14:03 a-h

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({});

kaioduarte avatar Aug 05 '21 20:08 kaioduarte

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?

kevindra avatar Aug 15 '21 21:08 kevindra

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 avatar Aug 15 '21 22:08 kevindra

@kevindra it worked for me image

kaioduarte avatar Aug 16 '21 09:08 kaioduarte

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

akshayr20 avatar Sep 13 '21 04:09 akshayr20

unfortunately mockImplementation on above spy still do not work, can someone guide me around same

akshayr20 avatar Sep 16 '21 06:09 akshayr20

@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.

a-h avatar Sep 16 '21 12:09 a-h

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

akshayr20 avatar Sep 16 '21 12:09 akshayr20

Any update on above issue, or any hint on where should I look into to debug this further?

akshayr20 avatar Sep 30 '21 14:09 akshayr20

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.

aecorredor avatar Oct 06 '21 18:10 aecorredor

Hey thanks @aecorredor I handled my situation using this approach https://github.com/evanw/esbuild/issues/412#issuecomment-938583697

akshayr20 avatar Oct 08 '21 12:10 akshayr20

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")
})

🤷‍♀️

alita-moore avatar Oct 23 '21 21:10 alita-moore

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')
})

EwertonLuan avatar Nov 15 '21 12:11 EwertonLuan

jest.mock('fooModule', () => {
  const originalModule = jest.requireActual('fooModule');

  return {
    ...originalModule,

    doThing: jest.fn().mockReturnValue('works!'),
  };
});

kirkstrobeck avatar Nov 26 '21 17:11 kirkstrobeck

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

marsonmao avatar Dec 09 '21 09:12 marsonmao

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(() => {});

vividn avatar Dec 15 '21 18:12 vividn

@EwertonLuan this is the best answer for me, not perfect but the cleanest from all, thanks

kamilzielinskidev avatar Mar 28 '22 16:03 kamilzielinskidev

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?

R-Iqbal avatar May 11 '22 14:05 R-Iqbal

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 }));

mywristbands avatar May 11 '22 19:05 mywristbands

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

WellingtonDefassio avatar Jul 13 '22 17:07 WellingtonDefassio

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!

DanielPe05 avatar Jul 14 '22 10:07 DanielPe05

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 🤷🏻

youf-olivier avatar Aug 10 '22 15:08 youf-olivier

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();
}) 

Darkzarich avatar Aug 11 '22 15:08 Darkzarich

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({});

gersondinis avatar Oct 12 '22 13:10 gersondinis

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({ ... });

Guy7B avatar Oct 20 '22 10:10 Guy7B

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 avatar Dec 07 '22 23:12 CalebJamesStevens

@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?

alexya avatar Dec 09 '22 03:12 alexya

+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.

image

baublet avatar Jan 18 '23 19:01 baublet

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?

ranran2121 avatar Mar 13 '23 11:03 ranran2121

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.

MisterJimson avatar Mar 30 '23 14:03 MisterJimson