Recently a colleague of mine, Justin Tullgren, made the point on Twitter that jQuery selectors are significantly slower than their (lesser known) native browser equivalents: querySelector and querySelectorAll. This revelation leads to exactly the sort of changes that black-box testing a view façade allows you to make because it will won't change its behavior even though it improves the process.
To demonstrate, consider the getCheckbox implementation from our example using jQuery.
var result = $('.sample :checkbox:first').eq(0);
if (result.length === 1) {
return result[0];
}
return null;
Using a jQuery to select our checkbox and return the DOM element or null
Since querySelector or querySelectorAll only operate on CSS selectors, we need to replace jQuery's :checkbox pseudo-class selector with the pure CSS equivalent of input[type="checkbox"]. We can avoid worrying about the CSS to replace the :first pseudo-class by using querySelector to only return the first element that matches the query.
All together, our replacement for the four lines (depending on how you count them) of code above is just a single line:
Using querySelector to get the DOM element for our checkbox with a CSS selector
This does introduce a new dependency into our function which we have to inject or resolve in order to maintain our unit of work's isolation.
Personally, I like how resolving the document reference from the already-injected jQuery dependency means we won't have to change any outside code.
function SampleViewFacade ($) {
var me = this,
document = $('body')[0].ownerDocument;
Declaring a new document variable from the ownerDocument of the body
I might try to justify this as my using jQuery as a consistent gateway to the DOM, but mostly I think it's cool!
The end of the story is that we have just introduced a significant cross-browser performance enhancement, while maintaining our application's behavior.
Go ahead and run the tests to prove it to yourself.
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:
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
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
defining the SampleViewFacade class
adding a getCheckbox method
accepting jQuery as a parameter in the constructor
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.
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.
Test-driven development in JavaScript is easy to the point of inducing maniacal laughter -- my office mates don't seem to appreciate this -- because its loosely-typed nature allows you to create mock implementations in-line with writing your tests.
Separately, jQuery brings joy to working with JavaScript in the browser by providing access to the DOM in a way that is natural and consistent.
Unfortunately, these twin marvels don't automatically become one euphoric JavaScript mega-experience when brought together. Instead, I've found a sort cultural impasse between TDD and jQuery. TDD forces you to think "what steps will I take to solve a problem," and jQuery tend to make you think "jQuery solves ALL THE THINGS!" The result is a collection of tests which are highly opinionated about how jQuery is used, instead of being solely opinionated about whether or not the problem is solved.
Let's take a simple example built-out using TDD to demonstrate what I mean by tests that are opinionated about jQuery's implementation and how this most often happens. If code examples bore you, feel free to skip past this one and go right to my conclusions.
Example: Mocking jQuery in TDD
For our example we'll develop an area of a page where a check box shows or hides a field set when it is clicked.
Build the Test Fixture
Step one is setting up a QUnit test fixture using jQuery's CDN to run our tests against:
In this, sample.tests.js contains our tests driving the code developed in sample.js which will actually perform the work we care about. In good TDD fashion, we could open this, see catastrophe and determine that our first step is to start writing our actual tests.
Our next step is to create sample.tests.js, and start it with our global variable and QUnit module declarations.
/*global module,test*/
var unit, $;
module('sample', {
setup: function () {
'use strict';
}
});
Here, unit is our unit under test, $ is our mock of jQuery and the comment at the top is a note to JSHint that we will be using the global names module and test from QUnit.
What Do We Want To Do?
Let's outline our test, to capture our intent and help us organize our thoughts:
test('clicking checkbox displays fieldset', function () {
'use strict';
});
Dissecting that further, what we expect that to mean in terms of testable events is:
The checkbox will be selected
A callback will be bound to the checkbox's click event which will
Select the fieldset
Toggle it to display or hide
Formalizing these expectations as asserts which will pass or fail we get:
var foundSelector,
foundClickCallback = null,
toggleCalled = false;
equal(foundSelector, '#uxShowFieldset', 'Checkbox selected by expected ID');
notEqual(foundClickCallback, null, 'Callback set for checkbox click');
// Asserts for callback
equal(foundSelector, '#uxFieldset', 'Fieldset selected by expected ID');
ok(toggleCalled, 'Toggle was called');
Next we'll mock jQuery as we expect it to be used in the implementation and capture the values that we're testing in our asserts:
// After our variable declaration
$ = function (selector) {
foundSelector = selector;
};
$.click = function (callback) {
foundClickCallback = callback;
};
$.toggle = function () {
toggleCalled = true;
};
Lastly we need to perform the actions to test with our mock jQuery injected into it:
// After defining our mock of jQuery
unit = new Sample($);
unit.init();
// Before our second set of asserts
foundClickCallback();
Note: I would ordinarily implement an init method as an IIFE within the constructor. I'm not doing that here, so that we can implement several alternatives for comparison.
Passing the Test
Following proper TDD practices, we will go through the following steps (running and failing the tests between each):
Create the sample.js file
Write the class function, Sample
Write the init function
Implement calls to jQuery to pass our asserts
If our tests are written well, the code that passes our test will work on the page. However, an astute reader might have already noticed that our tests are not well-written for jQuery. Consider the following implementation which will pass our test:
Our test assumed that our implementation would follow typical jQuery conventions and chain selectors to subsequent actions. If we were to do that our test would definitely pass, but my point here is that a developer could write broken code that doesn't pass our test -- and so we must account for that in our test.
Enforcing Good jQuery Usage
To account for that, we need to update our test so that, instead of finding specific selectors and asserting their correctness, it emulates jQuery's ability to chain by returning objects with click and toggle methods that we confirm are called.
In this mock, we only return an object with testable click and toggle methods for the exact instances when we expect them to be used -- to the point that the click method sets a flag to return the toggle-able object to guarantee that it is being called in response to the click event. This should force the developer to use jQuery correctly!
We have some new names in our mock, so our updated variable declaration becomes:
A final note on this mock is that one clear benefit is how we only need to assert that the field set was toggled, because that one test implies that everything else in the chain must have succeeded.
So the totality of our asserts becomes:
ok(fieldsetToggled, 'Fieldset was toggled on checkbox click');
Now we're ready to run it against our original implementation and see it fail! Once you've gotten that out of your system, point the test to a new function so that we can save the old implementation for comparison:
unit.init2();
Following the TDD process again, this test now enforces an implementation that works -- and looks like something you might see in practice:
What this example demonstrates is how natural it is to work through a test and assume jQuery will drive the implementation -- did you question for a second why we were injecting jQuery? -- and that ensure a good jQuery implementation properly you have to emulate its chaining in your mock.
Test Outcomes
The result of having tests that are so aware of jQuery is that their passage depends on the developer writing a specific implementation regardless of whether the outcome is correct or not.
For example, our current test will will fail if we refactor to use jQuery's on binding instead of click,
both of which are examples that improve our code, but will fail our test because the mock is, and has to be, so rigid. Which leads us to a...
New WebDev Law: A test specific enough to enforce something is done right mocking jQuery, is specific enough to enforce it being done just one way.
Working with tests that mock jQuery will condition a developer to expect tests to fail and require updating after any change in implementation, undermining the core assumption of TDD: the tests are correct.
View Façade
To avoid this, we can write a view façade which serves our class with exactly the functionality it needs -- in its terms -- and hides the jQuery used to provide it.
Revisiting our example, let's write a new test with this in mind.
We'll start with our first instincts and restate the asserts from the first test in the example, but in a non-jQuery way (i.e. no selectors):
var checkbox = {},
foundCheckbox,
foundClickCallback = null,
fieldset = {},
foundFieldset,
toggleCalled;
equal(foundCheckbox, checkbox, 'Correct checkbox found for binding click event');
notEqual(foundClickCallback, null, 'Callback for click event provided');
// Asserts for callback
equal(foundFieldset, fieldset, 'Correct fieldset found for binding click event');
ok(toggleCalled, 'Fieldset was toggled');
Now we'll mock out what we expect this new view façade to provide by appending a new facade to the variable declaration:
For this example, I've chosen to keep it simple by directly translating what our asserts expect into the methods that façade provides; this approach isn't required, what matters is that the façade serves the view terms that make sense within its context.
Note: the fact that toggleCalled is true is implied by foundFieldset being equal to fieldset, so we can refactor to remove toggleCalled.
The final step to building our test is to perform the actions to test with our mock façade:
// After defining our mock of the view facade
unit = new Sample(facade);
unit.init3();
// Before our second set of asserts
foundClickCallback();
Once we work through our implementation, it should look something like this (if the façade is called view locally):
view.bindClick(
view.getCheckbox(),
function () {
view.toggleElement(view.getFieldset());
}
);
By completely hiding jQuery from our Sample class, any changes to how the implementation within the view façade uses jQuery won't require an update to our tests.
jQuery is a DAL
This sort of encapsulation should seem familar to any developer who's worked in layered applications, because this is essentially the same sort of task-based abstraction applied by a business layer over a data layer. This isn't an accident, because jQuery, as the intermediary for reading from and writing to the DOM, is a data layer (among otherthings) -- we just don't typically see it that way, because it sits at the very opposite end of the stack from the traditional data layer.
That Solves Everything (Except the Original Problem)
Now we can avoid complicated jQuery mocks that make the tests for our view code fragile. The only cost is an additional small layer that we have to implement ... which works directly with jQuery.
Won't we have the exact same problem when we write tests for the view façade?
I'll post how to test the view façade while avoiding the same pitfalls in the next installment.
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.