WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
You can find the wrox.com code downloads for this chapter at www.wrox.com/go/reliablejavascript
on the Download Code tab. The files are in the Chapter 22 download and are individually named according to the filenames noted throughout this chapter.
We’ve occupied many pages of this book describing how unit tests can help ensure the reliability of non-visual components of a web application, namely the JavaScript conference’s website. There’s no doubt that components of the website that don’t have a user interface, such as the attendeeRegistrationService
, should have associated unit test suites.
Users of the website, however, aren’t aware that attendeeRegistrationService
exists. Even though users interact with the service, they don’t do so directly: They interact with it via the website’s UI, presented via a web browser.
It’s all well and good for an application to function correctly, but world-class software must please the end user. That means the user interface must function smoothly and quickly.
Unit tests for JavaScript that interacts with the browser’s document object model, or DOM, are one way to ensure that a web application’s UI functions properly. This section provides an example of what not to do when creating a UI, and addresses how the example can be refactored to be testable and reliable.
Have you encountered—or created—an HTML file similar to that in Listing 22-1?
There’s a <script>
element inside the <head>
tags, within which a variable, clickCount
, and a function, displayCount
, are defined.
The markup within the <body>
tags defines a <button>
, which, according to the value of the onclick
attribute, increments clickCount
and then executes displayCount
. There’s also a <span>
with the ID countDisplay
.
As you may have already determined, the code in Listing 22-1 simply tracks the number of times that the <button>
is clicked and displays that click count alongside the <button>
. Figure 22.1 shows how the markup renders when the page initially loads in the browser, and also how the page looks after the <button>
has been clicked a few times.
As Figure 22.1 shows, the interaction with the button works as described: Each click increments the count that’s displayed.
Even though the example functions as it should, Charlotte has a few suggestions.
First Charlotte points out that the HTML file doesn’t exhibit separation of concerns: The JavaScript responsible for responding to click events and displaying the count is written directly in the HTML file with the markup that defines the UI. This severely limits reusability because JavaScript defined in an HTML page may only be used by that page. Charlotte suggests that extracting the JavaScript into its own file will allow the JavaScript code to be reusable and to improve its testability.
Also, Charlotte notes that the clickCount
variable and displayCount
function are defined in the global scope. Even though doing so causes no detrimental effects in this example, creating global variables is poor practice when writing reliable JavaScript. Encapsulating the variable and function into a module, and enabling strict
mode, would ensure that the global scope isn’t polluted.
Additionally, Charlotte points out that the onclick
event handler contains multiple inline statements:
<button type="button" onclick="clickCount++; displayCount();">
Increment
</button>
While including multiple statements in the event handler is perfectly valid, it’s undesirable. A developer maintaining this code must inspect both the HTML and the JavaScript to get a complete picture of what happens when the button is clicked. Combining the statements into a single, descriptively named function such as incrementAndDisplayClickCount
would make the code easier to understand. Encapsulating that new method and adding code that can set it as the button’s click handler into a module would be even better. With a modular organization, it would be possible to understand the behavior being defined solely by examining the JavaScript. Creating a module containing the code would also improve its testability by allowing much of the logic to be tested without involving the UI at all.
Finally, the HTML elements and the JavaScript that interacts with them are tightly coupled. The coupling has multiple sources:
id
of the <span>
in which the count is displayed contained in the displayCount
function: var countElement = document.getElementById("countDisplay");
As long as the click handler is defined inline and the reference to the display element is hard-coded, the only way to reuse the code in the example is to copy and paste it and update the handler and element reference in the new copy. Creating a configurable module to encapsulate the code would break the coupling, allowing the click-counting and display logic to be used with multiple HTML elements.
The next section will begin the process of transforming the difficult-to-test, single-use code presented in Listing 22-1 into a testable, reusable UI component.
As you’ve done many times before, you’ll drive the development of the reusable UI component with unit tests. The Jasmine test framework supports UI unit tests without requiring any special setup or configuration when the tests are being executed within a browser, as the tests you’ll write in this section will be.
Consider the actions that the event handler code performs when the <button>
in Listing 22-1 is clicked:
<button>
has been clicked.Because manipulation of the UI is encapsulated into a function, you can create tests for the module that implements the enumerated behavior without yet being concerned with the DOM. The tests that ensure the click counting functionality works correctly follow in Listing 22-2.
The tests in Listing 22-2 ensure that the new module’s incrementCountAndUpdateDisplay
function performs the same high-level actions that the inline click event handler from Listing 22-1 performed. Specifically, the tests verify that executing incrementCountAndUpdateDisplay
causes the click count to be incremented and the function that updates the DOM to be executed. The tests also verify that the initial click count is 0.
Even though they’re simple, these initial tests help define the API that the new module needs to expose. They also give you a protection against introducing defects while extending and refactoring the module’s code. Figure 22.2 shows that the unit tests fail when the module’s API methods aren’t implemented.
You fill in the module’s API methods, yielding the code in Listing 22-3.
The initial implementation of Conference.clickCountDisplay
holds no surprises; it’s as simple as the unit tests that drove its completion. The module function initializes the hidden variable clickCount
to 0, and allows public retrieval of the variable via the function getClickCount
. The function incrementCountAndUpdateDisplay
does exactly what its name implies: It increments clickCount
and invokes the updateCountDisplay
method. Because the unit tests for incrementCountAndUpdateDisplay
don’t rely upon the implementation details of updateCountDisplay
, the method updateCountDisplay
doesn’t need to be implemented in order for the unit tests in Listing 22-2 to pass, as Figure 22.3 shows.
Your initial foray into testing DOM interaction will be to design tests for the updateCountDisplay
function. To test the updateCountDisplay
function, you need to be able to add an element to the DOM that the function can manipulate from within a test. It’s possible to add elements to the DOM using functions provided by the browser, but the ubiquitous jQuery library provides a browser-independent faç over browser-provided DOM interaction methods. Your tests for updateCountDisplay
will use jQuery to provide an element for updateCountDisplay
to update. Listing 22-4 shows the tests.
Because updateCountDisplay
changes the DOM based on the value of clickCount
, and clickCount
can only be changed via the incrementCountAndUpdateDisplay
function, updateCountDisplay
requires only a single unit test that ensures it behaves properly if it’s invoked before incrementCountAndUpdateDisplay
has been called. Additional testing of updateCountDisplay
is performed indirectly via incrementCountAndUpdateDisplay
.
We mentioned that tests for updateCountDisplay
would need to be able to add an element to the DOM. The beforeEach
section of the test suite was updated to add the necessary element:
// Create a jQuery element from a string that defines the DOM element
displayElement = $("<span></span>");
// and append it to the body
$('body').append(displayElement);
First, the <span>
element that will display the count is created. Then, the <span>
is appended to the <body>
of the HTML page that the Jasmine tests are running in. The <span>
element is also added to the new options
variable that is provided to the module function:
var options = {
updateElement : displayElement
};
display = Conference.clickCountDisplay(options);
The module will be able to access the span
element via the options.updateElement
property.
Listing 22-4 also introduced an afterEach
to the test suite. The afterEach
section performs only a single, but important, function: It removes the DOM element added in the beforeEach
. Neglecting to remove the element would result in <span>
elements accumulating on the page. Also, removing the element used in each test when the test completes reduces the likelihood that the order in which the tests are executed could change the outcome of the tests.
The single unit test for the updateCountDisplay
function uses a matcher function you may not have seen before to ensure the expected value is displayed in the DOM: toHaveText
. If you’ve reviewed the matcher functions that Jasmine provides, you’ll recognize that toHaveText
is not built-in to Jasmine. It’s provided by the open-source library jasmine-jquery, which is maintained by Travis Jeffery. The library provides dozens of matchers that are especially useful when testing JavaScript that interacts with the DOM.
The test for incrementCountAndUpdateDisplay
that was added to indirectly test the updateCountDisplay
method also makes use of the toHaveText
matcher. The test ensures that each time the incrementCountAndUpdateDisplay
is called, the incremented clickCount
value is displayed in the DOM.
Because you haven’t implemented the updateCountDisplay
function, the new tests fail, as Figure 22.4 shows.
The implementation that allows the new unit tests in Listing 22-4 to pass follows in Listing 22-5.
Listing 22-5 shows the updated module function that now accepts an options parameter. It also notes that verification of the options parameter has been consciously left out for this example.
Most importantly, Listing 22-5 provides an implementation of updateCountDisplay
that uses the text
method of the jQuery object provided via options.updateElement
to set the DOM element’s text
property to the value of clickCount
. With the addition of the implementation of updateCountDisplay
, all of the unit tests pass once again, as Figure 22.5 shows.
It’s worth noting that at this point, a major portion of the clickCountDisplay
module’s functionality is fully implemented, yet you haven’t had to make your tests click a DOM element. Because the actions performed when an element is clicked have been encapsulated into a function that may be unit-tested on its own, it’s not necessary to perform a click. Instead, the function can be invoked directly, as you’ve done in the tests to this point. In the next section, you’ll add tests for clickCountDisplay
that click an element and ensure that incrementCountAndUpdateDisplay
executes when the element is clicked.
If you recall the situation from Listing 22-1, the click event handler of the <button>
was set directly in the markup:
<button type="button" onclick="clickCount++; displayCount();">
Increment
</button>
You’ve already followed one of Charlotte’s suggestions while implementing clickCountDisplay
: You encapsulated the multiple statements originally defined inline in the click event handler into the incrementCountAndUpdateDisplay
function.
Another one of Charlotte’s suggestions was to give the module the capability to assign its incrementCountAndUpdateDisplay
function as the target element’s click handler so that the assignment doesn’t need to occur in markup. Doing so, Charlotte suggested, would improve the reusability of the module. Her last suggestion worked out well, so you decide to follow this one as well.
In true test-driven fashion, you create the unit tests in Listing 22-6.
Only one unit test? Really? Yes, really. By encapsulating the functionality as you have, you only need to ensure that clicking the specified trigger element executes the incrementCountAndUpdateDisplay
function. The tests for incrementCountAndUpdateDisplay
ensure that it, in turn, behaves as it should.
The changes to the test suite required to ensure the event handler is invoked begin in the beforeEach
block. As you did when testing with the display element, you create an element that the test will “click” and append that element to the body
of the HTML page the tests are executing in. That element is also provided to the clickCountDisplay
module function via the triggerElement
property of the options
variable. The afterEach
has a corresponding change to remove the element from the DOM when the test completes.
The new unit test spies on the incrementCountAndUpdateDisplay
method of the display
instance of the clickCountDisplay
module. The test then uses the jQuery trigger
method to trigger a click event on the element whose clicks are being counted. Finally, the test verifies that triggering the click event executed the incrementCountAndUpdateDisplay
function. The new test fails, as Figure 22.6 illustrates.
Listing 22-7 shows the updated implementation of the clickCountDisplay
module.
Instead of immediately returning the new instance created by clickCountDisplay
, the module function assigns it to the clickCounter
variable. Then the clickBinder
function is registered as a handler of the options.triggerElement
’s click event via the jQuery on
function. The clickBinder
function will invoke the clickCounter.incrementCountAndUpdateDisplay
function when the triggerElement
click event is triggered, causing the click count to be incremented and the display to be updated. Finally, clickCounter
is returned to the caller. With that, the unit tests all pass, as shown in Figure 22.7, and the implementation of the clickCountDisplay
module is complete.
The tests that you didn’t write in the preceding section are just as important as the tests that you did write. When writing unit tests for the UI, it’s tempting to test the appearance of the UI:
And so on and so forth.
While it’s possible to write unit tests for everything enumerated (and more), unit tests that verify visual appearance tend to be brittle. The UI of a web application is likely to change more often than any other aspect, quickly making any tests that validate appearance out-of-date.
Generally speaking, UI unit tests should be limited to functionality, such as:
<select>
contain the expected elements?Tests of visual appearance should more often than not be left to manual testing or screenshot-based testing tools that can quickly determine deviations from the expected appearance.
We’ve all done it: written code that is less than beautiful in order to achieve efficiency and the improved response time that goes with it. But was it worth it? How did we know? In this section, you will see how to use your browser’s profiler to answer those questions.
The task before you is simple. You just need to create a web page that lists attendees at the JavaScript conference, and their interests. There are several thousand, so efficiency might be a concern.
Figure 22.8 shows what the page will look like. (The attendees will come from all over the world. That’s why their names range from Hipapipige Baba to Zawet Zuziyukuku. Either that, or they are random.)
You quickly develop the HTML in Listing 22-8.
The <body>
element has an onload
attribute that invokes Conference.attendeePage .addAttendeesToPage()
, the function at the end of Listing 22-9.
A lot happens in the three lines of addAttendeesToPage
(at the end of the listing). The attendees are fetched (okay, randomly generated), sorted, and then displayed.
You run the page and performance isn’t too bad, but you will be handing your work off to Charlotte for final integration with the server so you don’t want to embarrass yourself. Is there any way its performance could be improved?
You decide to break out Chrome’s profiler and find out. (We are using Chrome because it is by far the most popular among developers. Internet Explorer offers a similar facility.)
To use the profiler, you follow these steps.
index.html
.There are three modes in which the data can be displayed. The Tree (Top Down) view is the most intuitive. This view lists the top-level functions and, for each one, how much time is spent in the function proper (the Self column) and how much time in the function plus all the functions it calls (the Total column).
The other choices are Heavy (Bottom Up) and Chart. You will see how to use those shortly.
In the Tree view of Figure 22.10, the list of functions happens to end with the only one you have any control over, namely addAttendeesToPage
. You can focus on that one alone by clicking on the arrows to expand the call tree. The result is shown in Figure 22.11.
You are not concerned with optimizing the fetchAttendees
function because it’s only a fake, but it is interesting to note that it took only 1.75 percent of the time within the addAttendees
function, in spite of all its random-number computations and string concatenations (refer back to Listing 22-9). Even sorting 5,000 attendees took only 11.57 percent of the time. By far the bulk of the time was spent in displayAttendee
and the functions it called. This is typical of programs in the browser. Accessing the DOM is usually more costly in terms of CPU time than other operations.
Scanning down Figure 22.11, notice that a lot of time is spent in the addInterest
function, particularly where it sets the innerHTML
property of the <td>
element devoted to the attendee’s interests. (innerHTML
’s accessor functions are the “anonymous functions” in Figure 22.11.) Here is the relevant part of Listing 22-9:
var tdInterests = document.createElement('td'),
isFirstInterest = true;
/*** Snipped for clarity. ***/
tdInterests.innerHTML = '';
attendee.getInterests().forEach(function addInterest(interest) {
if (!isFirstInterest) {
tdInterests.innerHTML += ', ';
} else {
isFirstInterest = false;
}
tdInterests.innerHTML += interest;
});
What could have come over you? This is exactly the sort of plodding, overly procedural code that a beginner with limited JavaScript vocabulary would write. Now that the profiler has drawn your attention to your profligate use of innerHTML
in this region of code, you realize that the whole mess could be replaced with the following:
tdInterests.innerHTML = attendee.getInterests().join(', ');
Incidentally, while it was safe to assume that the interests contained no cross-site scripting attacks because they came from a set of choices under the application’s control, the code that directly inserted the attendee’s names into the HTML plays fast and loose. If this were more than a proof-of-concept exercise, you would protect against a cross-site scripting attack as explained in https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
.
You make the switch (attendeePage_Improved.js in this chapter’s downloads) and run the profiler again. This time, the story is quite different (see Figure 22.12).
Total time in displayAttendee
has decreased from 411.5 milliseconds to 323.2. You achieved a reduction in response time of 21 percent with that one simple change. You make a note to yourself to minimize DOM access, especially in loops. (Incidentally, DOM updates that cause the page’s layout to be recomputed are often the most costly.)
You decide to reward yourself by taking a moment to look at the other options in the profiler.
With the Tree (Top Down) view, you were able to drill down the call stack. The Heavy (Bottom Up) view is the reverse. It shows how much time was spent in each function and lets you drill up to see from where it was called. Figure 22.13 shows this view for the original version of the code, drilling up from uses of the innerHTML
property.
You can see that the immediate callers of the anonymous function that sets innerHTML
were addInterest
and displayAttendee
, with calls from the former accounting for more CPU time than calls from the latter. Thus, the Heavy view would also have led you to focus on addInterest
.
Finally, Figure 22.14 presents the Chart view.
The Chart view is handy because it lets you zoom in on a time period. In Figure 22.14, the lower, downward-pointing flame graph reflects only the period chosen by the sliders (indicated by the heavy rectangle).
The lower graph shows the execution sequence in the horizontal direction and the call stack in the vertical. Thus, it shows onload
calling Conference.attendeePage.addAttendeesToPage
, which calls displayAttendee
, and so on. You can hover over any block in this graph and get details about it. In Figure 22.14, the mouse was over one of the displayAttendee
blocks, triggering the display of statistics in the lower left.
You’re feeling pretty good about your victory, but something is bothering you. When you coded displayAttendee
, you began the function with:
var table = document.getElementById('attendeeTable')
Because displayAttendee
is called for every one of the 5,000 attendees, you have an uneasy feeling that the repeated call to get the same element might slow things down. Would it be better to do this once, in the calling function, and pass the table element to displayAttendee
as a parameter? You thought the function’s interface was cleaner without the extra parameter, but what price have you paid for this nicety?
Here’s where the profiler can set your mind at rest. Figure 22.15 shows that the time spent in getElementById
amounts to only 2.0 milliseconds of the over 400 milliseconds spent in displayAttendee
. If someone wants to persuade you to consolidate the calls to getElementById
, performance can’t be the reason.
In this chapter, you saw how writing JavaScript directly in an HTML file and using inline event handlers can reduce the reusability and testability of your application’s UI manipulation code. You used TDD to develop a loosely coupled UI component that you can use throughout your application. Additionally, you saw how to avoid creating brittle UI unit tests. You also saw how to use Chrome’s profiler to identify where performance bottlenecks are—and where they aren’t. In browser-based programs, performance problems can arise if you access the DOM too frequently, especially for updates.
The next chapter dives deeper than we have so far into tools that help you follow your organization’s coding standards, not to mention your own good intentions.