Sinon.js quick tip: How to stub/mock complex objects, such as DOM objects

Tags:

Several of my readers have emailed me, asking about how to deal with more complex stubbing situations when using Sinon.js.

In this article, we’ll look at how to stub objects which are deeply nested, and when functions have more complex return values and they interact with other objects.

We’ll use DOM objects as a practical example, as they’re used quite often, and they can present several challenges when stubbing.

The problem in more detail

If you’ve used Sinon, you’ll know stubbing simple objects is easy (If not, check out my Sinon.js getting started article)

For example, we can do…

//to stub someObject.aFunction...
var stub = sinon.stub(someObject, 'aFunction');

But what if you have a more complex call?

document.body.getElementsByTagName('div')[0].getAttribute('data-example')

How on earth would you stub something like that?

Let’s find out!

Stubbing functions in a deeply nested object

Sometimes you need to stub functions inside objects which are nested more deeply.

For example, we used document.body.getElementsByTagName as an example above. How can you stub that?

The answer is surprisingly simple:

var getElsStub = sinon.stub(document.body, 'getElementsByTagName');

That’s it. This works regardless of how deeply things are nested. Things do get a bit more complex if you need to stub a result of a function call, which we’ll look at in a bit.

Stubbing an entire complex object

In some situations, you might want to stub an object completely. For example, let’s say we have a function which sets some attributes on an element:

function setSomeAttributes(element) {
  var id = element.id;
  element.setAttribute('data-id', id);
  element.setAttribute('data-child-count', element.children.length);
}

In a situation like this, the easiest way to stub this is to just create a new object which you can then pass in as a parameter in your test:

var elStub = {
  id: 'foo',
  children: [],
  setAttribute: sinon.stub()
};

Note that we used sinon.stub for the function. We could’ve used an empty “normal” function too, but this way we can easily specify the behavior for setAttribute in our tests, and we can also do assertions against it.

With more complex fake objects like this, it’s easy to end up with messy tests with a lot of duplication. I recommend using test helper functions to create complex stubs, as they allow you to easily reuse your stubs and other functionality. If you want to learn more about test helper functions, grab my free Sinon.js in the Real-world guide.

Stubbing complex return values

When working with real code, sometimes you need to have a function return an object, which is stubbed, but used within the function being tested. For example, let’s say we have a function which applies a CSS class to certain elements:

function applyClass(parent, cssClass) {
  var els = parent.querySelectorAll('.something-special');
  for(var i = 0; i < els.length; i++) { 
    els[i].classList.add(cssClass);
  }
}

In order to test the correct class is being applied, we need to stub both parent.querySelectorAll and the returned elements in the list.

We’ll apply what we learned so far:

it('adds correct class', function() {
  var parent = {
    querySelectorAll: sinon.stub()
  };
  var elStub = {
    classList: {
      add: sinon.stub()
    }
  };
  parent.querySelectorAll.returns([elStub]);
  var expectedClass = 'hello-world';
 
  applyClass(parent, expectedClass);
 
  sinon.assert.calledWith(elStub.classList.add, expectedClass);
});

The interaction between the different functions can be a bit tricky to see at first. But keep in mind they are just normal JS objects and normal JS functions, albeit with some Sinon.js sugar sprinkled on top.

First, we create a test-double for the parent parameter. We set a stub for querySelectorAll, as it’s the only property used in the function. We’ll use this stub to return a list of fake elements.

Then, we create a stub for the element. Since we need to verify the classList.add function is called, we add a classList property with an add stub function.

After we make parent.querySelectorAll return a list with the stubbed element in it, we can run the function we’re testing.

Finally, since we returned a stubbed class list, we can easily verify the result of the test with a Sinon assertion.

Putting it all together

Now that we know the pieces we need to deal with more complex stubbing scenarios, let’s come back to our original problem.

document.body.getElementsByTagName('div')[0].getAttribute('data-example')

Now you should have an idea on how to stub this kind of code in your tests.

Let’s see what it would look like:

var getEls = sinon.stub(document.body, 'getElementsByTagName');
var fakeDiv = {
  getAttribute: sinon.stub()
};
getEls.withArgs('div').returns([fakeDiv]);

With the above code, we could now verify in our tests that the getAttribute function is called correctly, or have it return specific values.

Conclusion

Dealing with complex objects in Sinon.js is not difficult, but requires you to apply different functionality together to make things work. Although we used DOM objects as an example here, you can apply these same methods to stub any kind of more complex object.