Skip to content

Instantly share code, notes, and snippets.

@tswaters
Last active April 15, 2024 10:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tswaters/cdb93cc79cd1fdc1144002a40fcaae27 to your computer and use it in GitHub Desktop.
Save tswaters/cdb93cc79cd1fdc1144002a40fcaae27 to your computer and use it in GitHub Desktop.
stubbing classes?

Stubbing Classes and their Constructors in Native ES6

It's getting to that point. All the major evergreen browsers have (mostly) functional class keyword. But what does this mean for unit tests and the ability to stub constructors? In short, the answer is 'no'.

Many will tell you that es6 classes are just sugar coated es5 functions with prototypal inheritance setup. Mostly true - but there are two considerations that make testing more difficult:

  • super is a magical keyword that calls the same method on the parent class. in cases with methods, easy enough to stub those, sinon.stub(parent.prototype, 'whatever') -- for super itself, there is no way to stub out the constructor call... normally not a huge deal, but...

  • classes are not added to the global scope. where once you could call sinon.stub(global, 'SomeFunction'), sinon.stub(global, 'SomeClass') (or under window in the browser), this will throw an error.

Now granted, you shouldn't expose to window - and if you're using node the sinon guys will be quick to point out that proxiquire allows you to overwrite what a module exports -- but maybe it's a browser script, and your src is split up and concatenated into a single file inside a closure - load that one file and yes any functions will be on window ... any classes, on the other hand, will not.

Right now my goal here is to not use any transpilers and get by with what major evergreen browsers support natively...For what I'm writing, TinyCrop, it's small enough that all the components can be under the same namespace and the goal of the project is to be as small as possible -- so right out of the gate the boilerplate for a module require system is a non-starter.

Using functions

Consider the following:

function Foo () {
  // ...etc...
}

Foo.prototype = {
  whatever: function () {}
  // ...etc...
}

function Bar () {
  Foo.call(this)
  this.baz = new Baz();
}

// do the unhold javascript prototype dance
var proto = Object.create(Foo.prototype)
proto.whatever = function () { /* ...etc... */ }
Bar.prototype = Object.create(proto)
Bar.prototype.constructor = Foo;

function Baz () {
 // literally, a metric shit-ton of stuff.
}
Baz.prototype = {
  whatever: function () {},
  // ...etc...
}

These tests for the Bar constructor are used to illustrate that both the parent and external function can be stubbed out without much trouble...

describe('Bar', () => {
  var fooConstructorStub;
  var bazConstructorStub;
  beforeEach(() => {
    fooConstructorStub = sinon.stub(global, 'Foo');
    bazConstructorStub = sinon.stub(global, 'Baz');
  })
  describe('#constructor', () => {
    it('should call foo constructor' => {
      var bar = new Bar();
      assert(fooConstructorStub.called)
      assert(bazConstructorStub.called)
      assert(bar.property == null) // eqeqeq=smart
    })
  })
})

Now with classes

class Foo () {
  constructor () {
    //...etc...
  }
  whatever () {
  }
  // ...etc...
}

// no unholy prototype dance here!
class Bar extends Foo () {
  constructor () {
    super()
    this.baz = new Baz()
  }
}

class Baz () {
  constructor () {
    // metric shit ton of stuff
  }
  whatever () {},
  // ...etc...
}

Considerably less verbose... and, don't get me wrong, I'm glad I don't have to do the unholy javascript prototype dance any more -- but now the tests are failing.

sinon.stub(global, 'Foo'); // Attempted to wrap undefined property Foo as function
sinon.stub(global, 'Baz'); // Attempted to wrap undefined property Baz as function

Crap. Classes aren't exposed to the global scope. They kind of hang around in a state that sinon has no way to stub them.

function test1 () {}
class test2 () {}
console.log(test1) // function
console.log(test2) // class
// now with this -- or could be global/window
console.log(this.test1) // function
console.log(this.test2) // undefined

So now in these tests whenever I instantiate Bar, I will most definitely call the foo constructor - not much can be done for that. My real problem is that Baz is also being called which as you might notice does a metric shit ton of stuff.

This is the reason stubs exist - but there nothing can be done for this.

So what can be done?

I've spent a little while thinking about this and the only thing I can really think of is to not use constructors. I mean, you still use them but they only have one line, ever:

class Baz {
  constructor (...opts) {
    // super() ? 
    this.initialize(...opts)
  }
  initialize () {
    // super.initialize() ?
    // metric shit ton of stuff
  }
}

This can be stubbed out --

initializeStub = sinon.stub(Baz.prototype, 'initialize');

constructor still gets called, but the metric shit ton of stuff that it does no longer happens and while it's not "Perfect", I'm content that I can get unit tests somewhat isolated... there will be no side effects of having the constructor called if the initialize method is the only thing that happens, and it is stubbed out.

@gagarwa
Copy link

gagarwa commented Dec 15, 2023

I didn't even know stubbing a class using prototype (ex: sinon.stub(Baz.prototype, 'initialize');) was possible. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment