CHAPTER 22
Testing DOM Access

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-TESTING UI

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.

Examining Difficult-to-Test UI Code

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.

images

Figure 22.1

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:

  • The definition of the click event handler inline
  • The hard-coded reference to the 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.

Creating a UI Component Using TDD

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:

  1. The code increments a variable that contains the number of times the <button>has been clicked.
  2. The code calls a function that updates the DOM.

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.

images

Figure 22.2

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.

images

Figure 22.3

Testing Code That Changes the DOM

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.

images

Figure 22.4

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.

images

Figure 22.5

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.

Testing to Ensure Event Handlers Are Executed

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.

images

Figure 22.6

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.

images

Figure 22.7

Keeping UI Tests from Being Brittle

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:

  • Are the DOM elements in the right place?
  • Are the DOM elements the correct size?
  • Is the correct font being used?
  • Is the background color correct?

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:

  • Is the correct handler executed when an element is clicked?
  • Are elements of the UI that the user is not authorized to see hidden?
  • Does a <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.

OPTIMIZING YOUR CODE WITH A PROFILER

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.

Detecting Inefficiencies

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.)

images

Figure 22.8

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.

  1. Launch the page. You can do this by downloading the Profiler directory in this chapter’s downloads and then double-clicking on index.html.
  2. Press F12 to open Chrome’s developer panel. (F12 is also the magic button for Internet Explorer.)
  3. Select the Profiles tab. One of the profiling options is Collect JavaScript CPU Profile. That is the one you want for this exercise. Make sure it is selected (see Figure 22.9).
  4. Press either the Start button or the circular, gray button in the upper-left corner of the Profiles tab (see Figure 22.9 again).
  5. Very quickly, press the refresh button on your browser.
  6. When the page comes back, press either the circular button again (which will have been red during profiling) or the Start/Stop button again (whose text will have changed from Start to Stop during profiling).
  7. A display like Figure 22.10 will appear. If necessary, select the “Tree (Top Down)” view
images

Figure 22.9

images

Figure 22.10

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.

images

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).

images

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.

images

Figure 22.13

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.

images

Figure 22.14

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.

Avoiding Premature Optimization

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.

images

Figure 22.15

SUMMARY

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.