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

Stubbing constructor does not work #1892

Closed
RenWenshan opened this issue Sep 6, 2018 · 17 comments
Closed

Stubbing constructor does not work #1892

RenWenshan opened this issue Sep 6, 2018 · 17 comments

Comments

@RenWenshan
Copy link

Describe the bug
Doesn't trigger stubbed constructor

To Reproduce
Run the following test, it will give AssertError: expected constructor to be called once but was called 0 times:

  import sinon from 'sinon';

  class FooBar {
    constructor() {
      console.log("hello");
    }

    f() {
      console.log('f');
    }
  }

  describe('Example', () => {
    it('stubs constructor', () => {
      sinon.stub(FooBar.prototype, 'constructor').callsFake();
      new FooBar();
      sinon.assert.calledOnce(FooBar.prototype.constructor); // fails
    });

    it('stubs method', () => {
      sinon.stub(FooBar.prototype, 'f').callsFake();
      const a = new FooBar();
      a.f();
      sinon.assert.calledOnce(FooBar.prototype.f); // passes
    });
  });

Expected behavior
First test passes (stubbed constructor gets called)

Screenshots
If applicable, add screenshots to help explain your problem.

Context (please complete the following information):

  • Library version: 5.0.7
  • Environment: macOS sierra
  • Other libraries you are using: mocha
@mantoni
Copy link
Member

mantoni commented Sep 6, 2018

You’re stubbing a reference to the constructor (prototype.constructor). It’s not possible to stub the constructor itself due to language constraints.

You can inject the constructor and then inject a plain stub in your test to verify that it was called with new.

@mantoni mantoni closed this as completed Sep 6, 2018
@fatso83
Copy link
Contributor

fatso83 commented Sep 6, 2018

@RenWenshan This issue has appeared multiple times, but it doesn't have anything to do with Sinon in itself, but is simply due to a missing understanding of what the constructor keyword in ES6 refers to (hint: it shares very little with Function#constructor other than the name).

What we as an org can do to improve the situation is to write tutorials and documentation, something that demands a bit of an effort, which is why we have an issue for tackling this (#1121). Until that materializes itself I suggest reading

That knowledge will make it vastly easier to work with your ES6 (and later) code, as you know what is really happening.

@RenWenshan
Copy link
Author

RenWenshan commented Sep 10, 2018

@fatso83 thank you so much for explaining this.

Now I see that constructor is a syntax sugar and it's nothing to do with Function.prototype.constructor, therefore stubbing it has no effect.

My original need was to stub the constructor of a class's parent class so I can test whether super was called, e.g.

class Foo {
  constructor() {
    console.log("Foo");
  }
}

class FooBar extends Foo {
  constructor() {
    super();
    console.log("FooBar");
  }
}

Since the underlying implementation of _inherits includes:

    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;

I think I can stub the __proto__ of Foo? But it doesn't seem to work:

    const stub = sinon.stub(FooBar, '__proto__').callsFake(() => {
      console.log("stub");
    });

    const foo = new FooBar(); // still outputs "Foo\nFooBar"

@fatso83
Copy link
Contributor

fatso83 commented Sep 10, 2018

Your thinking is sound, but the implementation uses deprecated elements.

(__proto__'s) existence and exact behavior has only been standardized in the ECMAScript 2015 specification as a legacy feature
Ref MDN

Basically, you shouldn't use it in your code.

There is a reason the _inherits function you posted a snippet from tries to use Object.setPrototypeOf if available, only falling back to using __proto__ in really old browsers. The reason is that it works.

Object.setPrototypeOf(B, sinon.stub().callsFake(function() { console.log('I am Super Stub!'); } ));
new B(); // prints am Super Stub!
Object.getPrototypeOf(B).callCount // 1

See this gist for more. Also this in case you wonder about ES5 inheritance.

@svennergr
Copy link

@fatso83 Thanks for the gist. That helped me stubbing classes.
But how do I spy on classes properly? I tried different ways, but the only way I could really spy on a constructor was the following:

  it('test sinon constructor spy with container', () => {
    const testClassContainer = {};
    testClassContainer.TestClass = class TestClass {
      constructor() {
        console.log('testClass created');
      }
    }
    const a = sinon.spy(testClassContainer, 'TestClass');
    console.log('called', a.called); // false
    new testClassContainer.TestClass();
    console.log('called', a.called); // true
  });

@fatso83
Copy link
Contributor

fatso83 commented Nov 29, 2018

That looks like a usage question; please post it to StackOverflow and tag it with sinon, so the bigger community can help answer your questions.

A lot more people follow that tag that will help you.

@simoneb
Copy link

simoneb commented Dec 9, 2020

Here's what works for me:

test('constructor', async () => {
  const constructorStub = sinon.stub()

  function MyClass (...args) {
    return constructorStub(...args)
  }

  new MyClass({ some: 'args' })

  sinon.assert.calledWith(constructorStub, { some: 'args' })
})

@fatso83
Copy link
Contributor

fatso83 commented Dec 9, 2020

@simoneb Are you sure that even makes sense simon? You just implemented the entire object to test as something that returns a stub. That is something that is not possible/makes sense in any production system.

This is not "stubbing the constructor", btw. This is creating a constructor that returns a function and testing if the returned function is called. The constructor is still MyClass - and that is not a stub (nor can it be).

@simoneb
Copy link

simoneb commented Dec 9, 2020

@simoneb Are you sure that even makes sense simon? You just implemented the entire object to test as something that returns a stub. That is something that is not possible/makes sense in any production system.

This is not "stubbing the constructor", btw. This is creating a constructor that returns a function and testing if the returned function is called. The constructor is still MyClass - and that is not a stub (nor can it be).

Yes this is not stubbing the constructor but when used in conjunction with something like proxyquire, which you can use to mock module exports, you can replace the constructor of an external library with MyClass and check how the constructor is invoked. I should have been clearer.

@Idono87
Copy link

Idono87 commented Feb 28, 2021

How about replacing the entire class as a stub instead?

import sinon from 'sinon';
import * as exampleModule from 'exampleModule';

describe('example', function(){
  it(function() {
    const classStubbedInstance = Sinon.createStubInstance(exampleModule.ExampleClass);
    const constructorStub = Sinon.stub(module, 'ExampleClass').returns(classStubbedInstance);

    sinon.assert.calledWith({some: 'args'})
  })
)

So far it's worked for some simple cases for me.

@andreisaikouski
Copy link

@Idono87 what is "module" in this case?

@Idono87
Copy link

Idono87 commented Nov 30, 2021

@Idono87 what is "module" in this case?

I really don't know what your asking. What is a module? What module am i referencing?

@hellokatili
Copy link

@Idono87
I guess @andreisaikouski is asking: what is module in this line:
const constructorStub = Sinon.stub(module, 'ExampleClass').returns(classStubbedInstance);

@Idono87
Copy link

Idono87 commented Dec 21, 2021

@Idono87
I guess @andreisaikouski is asking: what is module in this line:
const constructorStub = Sinon.stub(module, 'ExampleClass').returns(classStubbedInstance);

Missed that.

It's just a typo. Should be "exampleModule".

@DanKaplanSES
Copy link
Contributor

DanKaplanSES commented Jan 5, 2024

Object.setPrototypeOf(B, sinon.stub().callsFake(function() { console.log('I am Super Stub!'); } ));
new B(); // prints am Super Stub!
Object.getPrototypeOf(B).callCount // 1

@fatso83 This code works, but is it still the way sinon users should do it? Namely, has this been integrated into the sinon API? e.g., sinon.stubParentConstructor(Child)?

@NothingAmI
Copy link

NothingAmI commented Jan 5, 2024

Object.setPrototypeOf(B, sinon.stub().callsFake(function() { console.log('I am Super Stub!'); } ));
new B(); // prints am Super Stub!
Object.getPrototypeOf(B).callCount // 1

@fatso83 This code works, but is it still the way sinon users should do it? Namely, has this been integrated into the sinon API? e.g., sinon.stubParentConstructor(Child)?

@DanKaplanSES I initially loved this approach and it does work. But actually the routing to callsFake persists even after the restore call on the sandbox (in my case I use sinon sandbox). So the Object.setPrototypeOf has to be used and reset separately in addition to restoring the stub/sandbox. It's not actually stubbing the constructor here but replacing the prototype with a new function. It's similar to calling Object.setPrototypeOf(B, function(){console.log('....');}) which works without the stub.

@DanKaplanSES
Copy link
Contributor

@DanKaplanSES I initially loved this approach and it does work. But actually the routing to callsFake persists even after the restore call on the sandbox (in my case I use sinon sandbox).

Yeah, I think it has room for improvement so I created #2578

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants