Friday, September 6, 2013

jQuery in TDD with a View Façade (and Introducing Oreo Testing)

In my post jQuery in TDD is Serious Business (Don't Mock It), I argued that jQuery (and any DOM manipulation) should be isolated in a view façade to avoid mocking its behavior and creating brittle tests. This is fine advice for creating a resilient view layer, but it doesn't address how to avoid the same problem when the view façade needs to be tested.

White-Box Testing

While writing our view, we had an implicit goal to isolate it from everything external to it, including other units (i.e. using mocks) and third parties (e.g. encapsulating jQuery into a view façade). Besides ascending to a higher plane of code modularity, this isolation serves two critical purposes:
  1. allowing a tester to quickly identify the exact unit and test that is failing when a failure occurs without having to walk down the application stack.  In other words, a failure in testing equals a failure in exactly one unit
  2. freeing us to start and finish any unit of work without having to wait for a depended-upon unit to be written
In my experience, this leads to Test-Driven Development (TDD) practically assuming a white-box testing approach to development.

If we try and apply that wisdom that to our view façade however, we encounter all of the same problems we had been trying to avoid.  At the very edge of our stack, with nowhere left to push jQuery, we have to face the music.  We have to either mock jQuery or find another way.

Black-Box Testing

One of the major objections I raised to mocking jQuery, was how it turns virtually any code change into a test failure even if the unit's behavior hasn't changed.  If we rephrase that positively as “we want to write tests that pass, regardless of the implementation, so long as they produce the correct results,” we stumble upon black-box testing.

In black-box testing we prepare a test environment with known data, perform the tested action and then verify success against expected output.

In our example, this means creating a free-standing DOM for testing purposes, contextualizing a jQuery instance to it and passing that as the dependency to our view façade.

Example

Without going through the individual steps this time, let's set up a new test environment with QUnit and include jQuery itself on the test page this time.

As a refresher, the interface we defined for our view façade is:




facade = {
 getCheckbox: function () {
 },
 bindClick: function (element, callback) {
 },
 getFieldset: function () {
 },
 toggleElement: function (element) {
 }
};
I apologize for the horrible naming scheme. It can be so hard to make up with a decent example.

Let's dig in and write a quick outline for a test against the getCheckbox method:




test('getCheckbox returns the correct checkbox', function () {
 'use strict';
 var checkbox,
  foundCheckbox;
 
 unit = new SampleViewFacade($);
 foundCheckbox = unit.getCheckbox();
  
 equal(foundCheckbox, checkbox, '');
});
Test that instantiates the unit, calls the getCheckbox method and an assert comparing the returned value and a test value.

If we were writing a white-box test, our next step would be to mock jQuery to return checkbox as a test value for the expected call chain.  Instead we need to construct a test environment with a DOM supplied by the and a jQuery instance that interacts with that environment.

Contextualizing jQuery to a Mock DOM

To use jQuery with predictable data, I've written the following function to inject arbitrary HTML into the body element of an iframe and return a jQuery object confined to its context.

function getJqueryMockDocument (html) {
 var $ref, $doc;

 $('iframe#jqueryMockDocument').remove();
 $ref = $('<iframe id="jqueryMockDocument" style="display: none;">').appendTo('body').contents();
 $doc = $ref.extend(function (selector) { return $ref.find(selector); }, $ref);

 // For IE (head missing immediately on iframe add)
 if ($doc('head').length === 0) {
  $doc[0].write('<head>');
 }

 // For IE (body missing immediately on iframe add)
 if ($doc('body').length === 0) {
  $doc[0].write('<body>');
 }

 if (html) {
  $doc('body').append(html);
 }

 return $doc;
}

This function creates (or replaces) an IFrame on the page to allow access to elements and nodes outside of the body, such as head and document.

It then selects the contents of the body within that IFrame and extends it with a wrapper of jQuery's find method.  This is to complete the mock to allow $ selector calls.

The final part appends the html parameter to the body of the new IFrame.

The remainder is dealing with the fact that IE was confused, got up from the kid's table and wandered over to where adult browsers were talking.

Writing the Mock HTML

Now that we can convert an HTML fragment into a jQuery instance running against it as a DOM, let's put it to use!

Since our application is only as large as my contrived example, it's reasonable to assume that the "correct" checkbox will be the first checkbox within our application's namespace.  Our HTML will therefore be an outer div with a class for our namespace, sample, and a checkbox we expect to have returned.  For the sake of testing, we'll set an attribute on the checkbox to make confirming we've selected the correct checkbox easier.

Our updated variable declaration is now:
var foundCheckbox,
 $mock = getJqueryMockDocument('<div class="sample"><input id="correctElement" type="checkbox" />');

Our updated assert now is:
equal($(foundCheckbox).attr('id'), 'correctElement', 'Found the checkbox with the expected ID.');

Finally, with our contextualized jQuery we need to update our test statement to:
unit = new SampleViewFacade($mock);

Following the TDD process, we'll write code to make the test pass by
  1. defining the SampleViewFacade class 
  2. adding a getCheckbox method
  3. accepting jQuery as a parameter in the constructor
  4. selecting and returning the checkbox element
which looks like:
function SampleViewFacade ($) {
 var me = this;
 
 me.getCheckbox = function () {
  var result = $('.sample :checkbox:first').eq(0);
  
  if (result.length === 1) {
   return result[0];
  }
  
  return null;
 };
}

Black vs. White Box Testing

This example demonstrates that we can write a test that accurately validates success without mocking jQuery. If this is advantageous, and works for our view façade, then what’s the catch?  Why didn't we test the view this way?

Developing our view in TDD without any opinion of its internals would have meant:

  • being unable to use internal-aware mock dependencies
  • needing to wait for the view façade to be written
  • speculating on the view's requirements when defining the view façade interface
  • tests failing both if the view failed or if the view failed because the view façade failed

Since we have a workable white-box testing regime for developing our views, this isn't such a hard a pill to swallow. In terms of our view façade, the first three are similarly easy to accept, because they aren't problems in this case, but how do we handle the issue of isolating the view façade as a unit for testing?

Since the only dependency is jQuery and we have to operate under the assumption that it functions correctly and according to their documentation, there is no ambiguity when a test fails: it fails within our unit.  Therefore black-box testing has none of these shortcomings for the view façade, because the view façade is already isolated.

Oreo Testing

This example has focused on jQuery, but it applies to any third-party integration -- which implies an overall approach for TDD from end-to-end.

At either end of the application stack, where units interact with the host environment, black-box testing can be employed without losing any desirable aspects of white-box testing and should be employed to minimize test brittleness.

Within the interior of an application, where units only interact with other application units, white-box testing should be employed to isolate units, define interfaces and decrease wait time in development.

Black edges and white interiors

Download my complete code from this article
Browse the source on GitHub

Update 9/6/2013

As described in my later post, My "View Adapter" Was Just a Façade, I updated this post to refer to a "view façade" instead of a "view adapter" as it had originally been written.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.