Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: "TypeError: Cannot redefine property" when attempt to mock in module namespace #15532

Open
benf-teamglobalexp opened this issue Mar 4, 2025 · 1 comment

Comments

@benf-teamglobalexp
Copy link

benf-teamglobalexp commented Mar 4, 2025

Version

29.7.0

Steps to reproduce

When using the jest.mock method to mock a module namespace, the module is evidently not mocked.

When using the jest.spyOn method to mock a function in the imported module, JestJS crashes the test case with a "TypeError: Cannot redefine property".


A simple example: This module driver exports a function fetchDriversByState, and for test cases (testing other functions, that might internally call driver.fetchDriversByState), we don't want that function's implementation actually used.

// server/driver.ts

"use server";

export const fetchDriversByState = (async (state: string) => {
    const response = await fetch(`...`);
    // ...
});

// ... the real module has many additional exported properties, irrelevant to this demonstration ...

Writing test cases of a different area of the application, I need to mock this driver module so the driver.fetchDriversByState is a mock object. This needs to be done in some unit tests but not all, of course; the mock should replace the real function, only within the unit tests that don't require the real driver.fetchDriverByState implementation.


Attempt to mock the module namespace: jest.mock("@/server/driver")

// tests/input-form.test.tsx

import { describe, expect, jest, test } from "@jest/globals";
import "whatwg-fetch";

import * as driver from "@/server/driver";

jest.mock("@/server/driver");

describe("JestJS mock of import path '@/server/driver'", (() => {
    test(
        "should mock the imported module `driver`",
        (async () => {
            console.assert(jest.isMockFunction(driver));
            console.info("driver:", driver);
            console.assert(jest.isMockFunction(driver.fetchDriversByState));
            console.info(
                "driver.fetchDriversByState:", driver.fetchDriversByState);
            driver.fetchDriversByState("LOREM");
            expect(driver.fetchDriversByState).toHaveBeenCalled();
        }),
    );
}));

// ... the real suite has many additional test cases, some of which use or do not use the 'driver' module ...

The test case fails unexpectedly, because the expect.toHaveBeenCalled method determines the driver.fetchDriversByState function is not a mock:

 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● JestJS mock of import path '@/server/driver' › should mock the imported module `driver`

    expect(received).toHaveBeenCalled()

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function fetchDriversByState]

Attempt to "mock a partial" of the module: jest.mock("@/server/driver", () => { /* ... */ })

Using the "mocking partials" technique:

jest.mock('@/server/driver', () => {
    const originalModule = jest.requireActual('@/server/driver');

    return {
        __esModule: true,
        ...originalModule,
        fetchDriversByState: jest.fn(async (__) => 'fake drivers'),
    };
});

Results in the same failure: The test case fails unexpectedly because the expect.toHaveBeenCalled method determines the driver.fetchDriversByState function is not a mock:

 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● JestJS mock of import path '@/server/driver' › should mock the imported module `driver`

    expect(received).toHaveBeenCalled()

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function fetchDriversByState]

Attempt to mock the specific function: jest.spyOn(driver, "fetchDriversByState")

Yet, when I attempt to use jest.spyOn to mock that function specifically, I get a different error:

// tests/input-form.test.tsx

import { describe, expect, jest, test } from "@jest/globals";
import "whatwg-fetch";

import * as driver from "@/server/driver";

jest.spyOn(driver, "fetchDriversByState");

describe("JestJS mock of imported function 'driver.fetchDriversByState'", (() => {
    test(
        "should mock the imported function `driver.fetchDriversByState`",
        (async () => {
            console.assert(jest.isMockFunction(driver.fetchDriversByState));
            console.info(
                "driver.fetchDriversByState:", driver.fetchDriversByState);
            driver.fetchDriversByState("LOREM");
            expect(driver.fetchDriversByState).toHaveBeenCalled();
        }),
    );
}));

// ... the real suite has many additional test cases, some of which use or do not use the 'driver' module ...
 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● Test suite failed to run

    TypeError: Cannot redefine property: fetchDriversByState
        at Function.defineProperty (<anonymous>)

      12 |
      13 |
    > 14 | jest.spyOn(driver, "fetchDriversByState");
         |      ^
      15 |
      16 | describe("JestJS mock of imported function 'driver.fetchDriversByState'", (() => {
      17 |     test(

So: how can I mock this imported namespace using Jest mocks, and have that mock temporarily replace access at the driver.fetchDriversByState name?

Expected behavior

The driver namespace is valid, imported with import * as driver from "@/server/driver". So, this should be available to JestJS for mocking.

After jest.mock("@/server/driver"), the driver name should refer to a mock module, as described in the documentation.

After jest.spyOn(driver, "fetchDriversByState"), the driver.fetchDriversByState name should refer to a mock function, as described in the documentation.

Actual behavior

Attempt to mock the module namespace: jest.mock("@/server/driver")

 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● JestJS mock of import path '@/server/driver' › should mock the imported module `driver`

    expect(received).toHaveBeenCalled()

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function fetchDriversByState]

Attempt to "mock a partial" of the module: jest.mock("@/server/driver", () => { /* ... */ })

 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● JestJS mock of import path '@/server/driver' › should mock the imported module `driver`

    expect(received).toHaveBeenCalled()

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function fetchDriversByState]

Attempt to mock the specific function: jest.spyOn(driver, "fetchDriversByState")

 FAIL  src/components/form/test/demonstrating-module-mock.test.ts
  ● Test suite failed to run

    TypeError: Cannot redefine property: fetchDriversByState
        at Function.defineProperty (<anonymous>)

      12 |
      13 |
    > 14 | jest.spyOn(driver, "fetchDriversByState");
         |      ^
      15 |
      16 | describe("JestJS mock of imported function 'driver.fetchDriversByState'", (() => {
      17 |     test(

Additional context

The module to be mocked is a NextJS server module. I don't know whether this makes a difference. If it does, how do we use this "mock a specific module for a specific test case" technique with NextJS?

The module to be mocked does not export a default, by design; it is intended that the module be imported as a namespace (import * as driver from "@/server/driver"). The unit test imports it that way deliberately. This should not make a difference; driver is still a module, the driver.fetchDriversByState is still a function to be mocked. How do we use the mock feature to achieve this?

Environment

System:
    OS: Windows 11 10.0.22631
    CPU: (22) x64 Intel(R) Core(TM) Ultra 7 165H
  Binaries:
    Node: 23.9.0 - C:\Program Files\nodejs\node.EXE
    npm: 11.0.0 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    jest: ^29.7.0 => 29.7.0
Copy link

github-actions bot commented Apr 4, 2025

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions bot added the Stale label Apr 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant