Stubbing jQuery Selectors For Better Encapsulation in Unit Tests

I use a lot of jQuery at work for DOM manipulation and interaction in our web apps. In our unit tests, we tended not to stub out this external dependency, and made sure to use appropriate HTML fixtures so that jQuery calls can give us the expected outputs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
describe('AnalogClockWidget', function() {
  describe('on resize', function() {
    it('should recreate the clock circle', function() {
      var clockHTML = '<div class="clock">' +
        '<div class="short-hand"></div>' +
        '<div class="long-hand"></div>' +
        '</div>';

      $('#jasmine_content').html(clockHTML);

      // expectations down here...
    });
  });

  // More tests...
});

More recently, however, I was tasked to make use of the jstree plugin. Following the same theme of using HTML fixtures, I got something like this:

1
2
3
4
5
6
7
8
describe('SomeTree', function() {
  it('reconstructs the tree', function() {
    var content = '<ul class="jstree-container-ul jstree-children" role="group"><li role="treeitem" data-node-id="12" aria-selected="false" aria-level="1" aria-labelledby="j1_1_anchor" aria-expanded="false" id="j1_1" class="jstree-node  jstree-closed jstree-last"><i class="jstree-icon jstree-ocl" role="presentation"></i><a class="jstree-anchor" href="#" tabindex="-1" id="j1_1_anchor"><i class="jstree-icon jstree-themeicon" role="presentation"></i>MyString(+)</a></li></ul>'
    $('#jasmine_content').html(content)

    // more setup and expectations here...
  })
})

As you can see, it does not look pretty. The content HTML is showing us a lot of the innards of the jstree plugin, a lot of which should be black box – from my perspective as a client of third-party plugins at least. We could perhaps load the HTML fixtures and hide the horrendous HTML if we use veselin/jasmine-jquery gem:

1
2
3
loadFixtures('myfixture.html')
$('#my-fixture').myTestedPlugin()
expect($('#my-fixture')).to...

However, that would add another Mystery Guest to this test, and still not solve the issue of having irrelevant information. What we care about in this jstree HTML example, as far as encapsulation and my application logic is concerned, is that each of the li tags have data-node-id attributes, and nothing else, but those attributes are being hidden by other attributes and boilerplate tags like aria-selected, aria-level, etc. Those attributes and tags are vital for jstree to work, they just make it harder for other software developers to understand what really matters, in the context of the unit test.

Solution: Stub jQuery Selectors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// in the context of 'beforeEach' or 'it' block

// var positionValSpy, etc.

positionValSpy = jasmine.createSpy('val')
parentIdValSpy = jasmine.createSpy('val')
showModalSpy = jasmine.createSpy('showModal')
closeSpy = jasmine.createSpy('close')
cancelSpy = jasmine.createSpy('cancel')
htmlSpy = jasmine.createSpy('html')
appendSpy = jasmine.createSpy('appendSpy')
getSgdObjectsSpy = spyOn($, 'get')

jQueryStub = function(param) {
  switch(param) {
    case '#sgd_node_position':
      return { val: positionValSpy };
    case '#sgd_node_parent_id':
      return { val: parentIdValSpy };
    case '#sgd_node_sgd_object_id':
      return { html: htmlSpy, append: appendSpy };
    case '#nodeDialog':
      return [
        { showModal: showModalSpy, close: closeSpy }
      ];
    case '#cancel':
      return { click: cancelSpy };
    case '.new_sgd_node input[type="submit"]':
      return { click: function() { } };
    default:
      throw "Error: Did not recognize the selector";
  }
}

spyOn(window, '$').and.callFake(jQueryStub)

Stubbing jQuery lets us do queries like $('#sgd_node_position').val(), which will return us fake objects that just have the right amount of information we need in the context of a unit-test.