3

So I'm doing a little bit of experimental programming to ensure I understand how certain things work, and I'm coming across an error which I don't understand. I can't see any obvious solutions for this problem using Google or on here specifically.

I'm trying to confirm to myself exactly how jest.spyOn() works when applied to module imports. But right now the answer is that it just doesn't.

spy.js:

export const foo = () => {
  console.log("called foo");
};

export const bar = () => {
  console.log("called bar");
};

spy.spec.js:

import * as Foo from "./spy";
import { jest } from "@jest/globals";

test("call foo", () => {
  jest.spyOn(Foo, "bar");
});

package.json:

{
  "type": "module",
  "scripts": {
    "test": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
  },
  "dependencies": {
    "jest": "^27.0.1"
  }
}

When I run this test, I get the following error: TypeError: object.hasOwnProperty is not a function. I'm running node v15.12.0.

Can somebody explain to my

  1. Why this error occurs
  2. What I can do to remove it

Edit

It has been suggested that the problem is to do with the module import lacking a prototype property. This is not the issue - see below

import * as Foo from "./spy";
import { jest } from "@jest/globals";

test("SpyOn plain object", () => {
  const bar = { baz: () => {} };
  expect(bar.prototype).toBeUndefined(); // Pass
  jest.spyOn(bar, "baz"); // No error
});

test("SpyOn import", () => {
  expect(Foo.prototype).toBeUndefined(); // Pass
  jest.spyOn(Foo, "foo"); // Error!
});
Ben Wainwright
  • 4,224
  • 1
  • 18
  • 36
  • "*I'm experimenting to ensure I understand how certain things work*" - are you trying to understand how modules work, or are you trying to understand how jest spies work, or how jest's ability to mock modules works? – Bergi Jun 01 '21 at 16:53
  • "*It has been suggested that the problem is to do with the module import lacking a `prototype` property.*" - where? Can you link that suggestion please? And no, they didn't mean [the lack of a `.prototype` property, but the lack of a prototype](https://stackoverflow.com/q/9959727/1048572). Module namespaces do not inherit from `Object.prototype`. `Foo.hasOwnProperty` is undefined. – Bergi Jun 01 '21 at 16:56
  • 1
    @Bergi: My guess was the usage of arrow functions which lacks the prototype property, Ben was referring to my answer below. – Thalaivar Jun 01 '21 at 16:58
  • @Thalaivar correct – Ben Wainwright Jun 02 '21 at 07:56

2 Answers2

4

I debugged spyOn to see what happens.

Since Foo is an object of type Module, it doesn't have an hasOwnProperty method (see this answer regarding module namespace exotic objects).

Hence, spyOn throws in the following line (the object is the module Foo):

const isMethodOwner = object.hasOwnProperty(methodName); 

If we change it to:

const isMethodOwner = Object.prototype.hasOwnProperty.call(object, methodName);

It would work, but we'll encounter another error(s) in the code that follows, for example here:

object[methodName] = original;

Which throws:

TypeError: Cannot assign to read only property 'bar' of object '[object Module]'

So maybe it's "better" to just reassign object?:

object = Object.assign({}, object);

In which case the following test passes:

test("call bar", () => {
  const spy = jest.spyOn(Foo, "bar");
  spy();
  expect(spy).toHaveBeenCalled();
});

But the following test fails, though I can't tell if it should pass at all?:

test("call bar", () => {
  const spy = jest.spyOn(Foo, "bar");
  Foo.bar();
  expect(spy).toHaveBeenCalled();
});

Let us open a jest issue?

OfirD
  • 9,442
  • 5
  • 47
  • 90
  • `Cannot assign to read only property 'bar' of object '[object Module]'` means that intercepting `Foo.bar` by spying on `Foo` is not possible. This is not a Jest bug. – Bergi Jun 01 '21 at 18:51
  • You want a link to an edited file? ok, added. As for the `Cannot assign` error, if jest can't intercept `Foo.bar`, then shouldn't it be regarded as a jest bug? – OfirD Jun 01 '21 at 19:01
  • It's not a Jest bug, it's "works as intended" or "wontfix". Nothing can spy on an immutable object. – Bergi Jun 01 '21 at 19:11
  • 1
    Added the link to the source file. – OfirD Jun 01 '21 at 19:13
  • 1
    @OfirD thanks! It says I can award you the bounty in 5 hours, so I'll click the button later. – Ben Wainwright Jun 02 '21 at 08:00
2

I just added the missing method.

Foo.hasOwnProperty = () => Object.hasOwnProperty;
jest.spyOn(Foo, "foo")

Not ideal... but worked. I was using Vue and Jest and solved the error for me.