4

During unit testing, I want to Mock class Foo with a class called Bar. I have to change a propriety in the class Foo for the test.

This is what I tried. It should print:

Foo

Bar

class Foo {
  id = 'foo'
  constructor() {
    console.log(this.id)
  }
}
const Bar = { ...Foo }
Bar.prototype.id = 'Bar'
new Foo()
new Bar()

I get instead:

error: Uncaught TypeError: Cannot set property 'id' of undefined

I know I can achieve this by creating a new class Bar extends Foo but is there other possible solutions ?

Code Maniac
  • 37,143
  • 5
  • 39
  • 60
TSR
  • 17,242
  • 27
  • 93
  • 197
  • not really sure what you're trying to do, but you're probably trying to use a setter to update a class's property. Maybe this will help https://stackoverflow.com/questions/1535631/static-variables-in-javascript/45863870 Or just look up on what classes are supposed to do altogether https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes. – A. L May 02 '20 at 04:16
  • When you "copy" Foo using spread syntax, it only copies enumerable properties. Foo's *prototype* property is not enumerable so isn't copied. *Bar* doesn't have a public *prototype* property, so trying to assign to it is a TypeError. You can't create a constructor this way: *Bar* is a plain object, constructors must be functions. – RobG May 02 '20 at 04:27
  • BTW, [public field declarations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Defining_classes) (e.g. `id = 'foo'`) are experimental and will fail in some browsers at least. – RobG May 02 '20 at 04:32

2 Answers2

2

Object Spread {...o} only considers enumerable properties.

From MDN (emphasis mine):

Spread in object literals The Rest/Spread Properties for ECMAScript proposal (ES2018) added spread properties to object literals. It copies own enumerable properties from a provided object onto a new object.

The prototype property of classes and function is not enumerable. Therefore, in const Bar = {...Foo}, Bar will not have a prototype property. That is precisely why you get

error: Uncaught TypeError: Cannot set property 'id' of undefined

Now, to get the code closer to what you want, you could write

const Bar = {...Foo, prototype: Foo.prototype};

However, now Bar will behave differently from Foo because its prototype property will be enumerable.

So we could revise this and write

const Bar = {...Foo};
Object.defineProperty(Bar, 'prototype', {value: Foo.protoype, enumerable: false});
// `enumerable` defaults to `false` using this API, just being explicit for exposition.

However, your next statement, new B will still fail because B is still not a class object or a function object and therefore cannot be used with the new operator.

Without more context it is difficult to tell why you actually want to do this.

The most straightforward approach would be to write the following

class Foo {
  id = 'Foo';
  constructor() {
    console.log(this.id);
  }
}
class Bar extends Foo {
  id = 'Bar';
}
new Foo() // logs 'Foo'
new Bar() // logs 'Bar'

If you would like to avoid using inheritance, there are other alternatives, but it is not clear which ones are appropriate because the question lacks sufficient context to determine this. For example, if you only want to mock objects created via new Foo, then you could mock them trivially using a helper function that just overwrites the property with the test value, if you want to mock Foo itself, that is to say the class object, then the alternatives would be different.

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
1

Instance properties in NodeJS and Babel are not actually set to the prototype. They are set in the constructor.

Check this:

class Foo {
  id = 1
}

new Foo().hasOwnProperty('id') // => true

An object will only lookup a property on its prototype if it's not defined within itself, so overriding the prototype won't work.

If you want to avoid class syntax, you could do something like this:

function Bar() {
  const o = new Foo()
  o.id = 'bar'
  return o
}

new Bar().id // => 'bar'
ichigolas
  • 7,595
  • 27
  • 50