Mock construction of es6 collaborator

274 Views Asked by At

I have a class that leverages helper classes, and I'd like to verify it constructs those objects correctly. So, I'm trying to stub the "constructor" method in my classes, but I'm clearly not doing it right:

"use strict";

class Collaborator {
  constructor(settings) {
    console.log("Don't want this to be called!")
    this.settings = settings;
  }
}

class ThingToTest {
  constructor(settings) {
    this.helper = new Collaborator(settings);
  }
}

const assert = require("assert");
const sinon = require("sinon");

describe("ThingToTest", () => {
  let settings = "all the things"

  context("spying on constructor", () => {
    let spy = sinon.spy(Collaborator, "constructor")
    after(() => spy.restore()) 
    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        new ThingToTest(settings);
        sinon.assert.calledWith(spy, settings)
      })
    })
  })

  context("spying on prototype constructor", () => {
    let spy = sinon.spy(Collaborator.prototype, "constructor")
    after(() => spy.restore()) 
    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        new ThingToTest(settings);
        sinon.assert.calledWith(spy, settings)
      })
    })
  })

  context("stub constructor", () => {
    before(() => {
      sinon.stub(Collaborator, "constructor", (settings) => {
        console.log("This should be called so we can inspect", settings);
      })
    })
    after(() => { Collaborator.constructor.restore() }) 
    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        new ThingToTest(settings);
      })
    })
  })

  context("stub prototype constructor", () => {
    before(() => {
      sinon.stub(Collaborator.prototype, "constructor", (settings) => {
        console.log("This should be called so we can inspect", settings);
      })
    })
    after(() => { Collaborator.prototype.constructor.restore() }) 
    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        new ThingToTest(settings);
      })
    })
  })

})

Running this produces these (undesirable) results:

ThingToTest
    spying on constructor
      constructor
Don't want this to be called!
        1) creates a Collaborator with provided settings
    spying on prototype constructor
      constructor
Don't want this to be called!
        2) creates a Collaborator with provided settings
    stub constructor
      constructor
Don't want this to be called!
        ✓ creates a Collaborator with provided settings
    stub prototype constructor
      constructor
Don't want this to be called!
        ✓ creates a Collaborator with provided settings

It seems like stubbing is sort of working since putting the stub tests before the spy tests errors with the dreaded "TypeError: Attempted to wrap constructor which is already wrapped". So, clearly figuring out how to mock the Collaborators constructor is only half of what I'm doing wrong . . . I'm not restoring the constructor correctly either. Any suggestions?

2

There are 2 best solutions below

0
On

Not sure this is my final answer, but I ended up using proxyquire, as it's the best solution I've found so far. To show how it works, I've separated the classes under test into their own directory, and the test file in a child "test" directory. This illustrates how the paths in proxyquire work (which took me some time to figure out). So, here's what I ended up with:

/Collaborator.js

"use strict"

class Collaborator {
  constructor(settings) {
    console.log("Don't want this to be called!")
    this.settings = settings;
  }
}

module.exports = Collaborator

/ThingToTest.js

"use strict"

const Collaborator = require("./Collaborator")

class ThingToTest {
  constructor(settings) {
    this.helper = new Collaborator(settings)
  }
}

module.exports = ThingToTest

/test/ExampleTest.js

"use strict";

const proxyquire = require('proxyquire')
const mockCollaborator = sinon.stub();
const ThingToTest = proxyquire("../ThingToTest", { "./Collaborator" : mockCollaborator })

const assert = require("assert");
const sinon = require("sinon");

describe("ThingToTest", () => {
  let settings = "all the things"

  context("checking Collaborator in a more integration style test", () => {

    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        let thing = new ThingToTest(settings);
        assert.equal(mockCollab.firstCall.calledWith(settings))
      })
    })
  })
})

Note how the path inside proxyquire("../ThingToTest", { "./Collaborator" : mockCollaborator }) matches what "ThingToTest" uses, not the path from the test class. I hope this help others, but I'm still open to other ideas and suggestions!

0
On

This is not the solution I want, however for the time being I may end up using this (but please, if you have a suggestion, save me from myself):

context("checking Collaborator in a more integration style test", () => {
    describe("constructor", () => {
      it("creates a Collaborator with provided settings", () => {
        let thing = new ThingToTest(settings);
        assert.equal(thing.helper.settings, settings)
      })
    })
  })

This passes and verifies the Collaborator has the correct settings set. But now if I want to refactor the Collaborator constructor, I'm going to break the ThingToTest. Again, I'm still holding out hope someone can suggest a way to actually unit test this class!