Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jest spyOn not working on index file, cannot redefine property

I have UserContext and a hook useUser exported from src/app/context/user-context.tsx. Additionally I have an index.tsx file in src/app/context which exports all child modules.

If I spyOn src/app/context/user-context it works but changing the import to src/app/context I get:

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

Why is that?

Source code:

// src/app/context/user-context.tsx

export const UserContext = React.createContext({});

export function useUser() {
  return useContext(UserContext);;
}

// src/app/context/index.tsx

export * from "./user-context";
// *.spec.tsx

// This works:
import * as UserContext from "src/app/context/user-context";

// This does not work:
// import * as UserContext from "src/app/context";

it("should render complete navigation when user is logged in", () => {

    jest.spyOn(UserContext, "useUser").mockReturnValue({
        user: mockUser,
        update: (user) => null,
        initialized: true,
    });
})
like image 338
Code Spirit Avatar asked Jun 07 '21 13:06

Code Spirit


People also ask

Why can't I use jest spyon on a function?

Note that the properties are all defined with only get. Trying to use jest.spyOn on any of those properties will generate the error you are seeing because jest.spyOn tries to replace the property with a spy wrapping the original function but can't if the property is defined with only get.

Why can't I redefine an object defineproperty?

You are unable to redefine the property because Object.defineProperty () defaults to non-configurable properties, from the docs: configurable. true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. Defaults to false.

Why can't I mock the export of a jest object?

and the error you get is due to the compiler not adding configurable: true in the options for defineProperty, which would allow jest to redefine the export to mock it, from docs true if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.

Why can't I redefine the property defined in __Proto__?

Be careful with __proto__. You are unable to redefine the property because Object.defineProperty () defaults to non-configurable properties, from the docs: true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. Defaults to false.


Video Answer


3 Answers

If you take a look at the js code produced for a re-export it looks like this

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _context = require("context");

Object.keys(_context).forEach(function (key) {
  if (key === "default" || key === "__esModule") return;
  if (key in exports && exports[key] === _context[key]) return;
  Object.defineProperty(exports, key, {
    enumerable: true,
    get: function get() {
      return _context[key];
    }
  });
});

and the error you get is due to the compiler not adding configurable: true in the options for defineProperty, which would allow jest to redefine the export to mock it, from docs

configurable

true if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. Defaults to false.

I think you could tweak your config somehow to make the compiler add that, but it all depends on the tooling you're using.

A more accessible approach would be using jest.mock instead of jest.spyOn to mock the user-context file rather than trying to redefine an unconfigurable export

it("should render complete navigation when user is logged in", () => {
  jest.mock('./user-context', () => {
    return {
      ...jest.requireActual('./user-context'),
      useUser: jest.fn().mockReturnValue({
        user: {},
        update: (user: any) => null,
        initialized: true
      })
    }
  })
});

like image 108
diedu Avatar answered Oct 26 '22 21:10

diedu


The UserContext when re-exported from app/context/index.tsx throws that issue since it's a bug with Typescript on how it handled re-exports in versions prior to 3.9.

This issue was fixed as of version 3.9, so upgrade Typescript in your project to this version or later ones.

This issue was reported here and resolved with comments on the fix here

below is a workaround without version upgrades.

Have an object in your index.tsx file with properties as the imported methods and then export the object.

inside src/app/context/index.tsx

import { useUser } from './context/user-context.tsx'

const context = {
  useUser,
  otherFunctionsIfAny
}

export default context;

or this should also work,

import * as useUser from './context/user-context.tsx';

export { useUser };

export default useUser;

Then spy on them,

import * as UserContext from "src/app/context";

it("should render complete navigation when user is logged in", () => {

    jest.spyOn(UserContext, "useUser").mockReturnValue({
        user: mockUser,
        update: (user) => null,
        initialized: true,
    });
});

Ref

Good to Know:- Besides the issue with re-exports, the previous versions did not support live bindings as well i.e., when the exporting module changes, importing modules were not able to see changes happened on the exporter side.

Ex:

Test.js

let num = 10;

function add() {
    ++num;  // Value is mutated
}

exports.num = num;
exports.add = add;

index.js

enter image description here

A similar issue but due to the import's path.

The reason for this error message (JavaScript) is explained in this post TypeError: Cannot redefine property: Function.defineProperty ()

like image 33
deechris27 Avatar answered Oct 26 '22 19:10

deechris27


Well, people around suggest to use jest.mock() (as in this answer).

This is not aways good, because with jest.mock() you should mock functions at the top of your test spec.

I mean, some tests might be needed the different things to be mocked or stay real.

But then I find out that you can do this.

Put

import * as Foo from 'path/to/file'; 

jest.mock('path/to/file', () => {
  return {
    __esModule: true,    //    <----- this __esModule: true is important
    ...jest.requireActual('path/to/file')
  };
});

...

//just do a normal spyOn() as you did before somewhere in your test:
jest.spyOn(Foo, 'fn');

P.S. Also could be an one-liner:

jest.mock('path/to/file', () => ({ __esModule: true, ...jest.requireActual('path/to/file') }));
like image 16
SpellSword Avatar answered Oct 26 '22 19:10

SpellSword