6.8. Testing Java Script and AJAX

Even our simple AJAX example has many moving parts. In this section we show how to test it using Jasmine, an open-source JavaScript TDD framework developed by Pivotal Labs. Jasmine is designed to mimic RSpec and support the same TDD practices RSpec supports. The rest of this section assumes you’ve read Chapter 8 or are otherwise proficient with TDD and RSpec; as Figure 6.16 shows, we will reuse all those TDD concepts in Jasmine.

To start using Jasmine, add the jasmine-rails and jasmine-jquery-rails gems to the development and test groups in your Gemfile,and run bundle as usual, then run the commands in Figure 6.17 from your app’s root directory. Create a simple example spec file spec/javascripts/basic_check_spec.js containing the following code:

rails generate jasmine_rails:install
mkdir spec/javascripts/fixtures
git add spec/javascripts
describe('Jasmine basic check', function() {
    it('works', function() { expect(true).toBe(true); });
});
Figure 6.17: Creating the Jasmine-related directories in your app. Line 1 creates a spec/javascripts directory where our tests will go, with subdirectories support and helper analogous to RSpec’s setup. Line 2 adds a subdirectory for fixtures (Section 8.6). Line 3 adds these new JavaScript TDD files to your project.

To run Jasmine tests, just start your app as usual with the rails server command, and once it’s running, browse to the specs subdirectory of your app (so, for example, https://workspace–username.c9users.io/specs if running on Cloud9, or http://localhost:3000/specs if developing on your own computer) to run all the specs and see the results. From now on, when you change any code in app/assets/javascripts or tests in spec/javascripts, just reload the browser page to rerun all the tests.

Testing AJAX code must address two problems, and if you have read about TDD in Chap- ter 8, you’re already familiar with the solutions to both. First, just as we did in Section 8.4, we must be able to “stub out the Internet” by intercepting AJAX calls, so that we can return “canned” AJAX responses and test our JavaScript code in isolation from the server. We will solve this problem using stubs. Second, our JavaScript code expects to find certain elements on the rendered page, but as we just saw, when running Jasmine tests the browser is viewing the Jasmine reporting page rather than our app. Happily, we can use fixtures to test JavaScript code that relies on the presence of certain DOM elements on the page, just as we used them in Section 8.6 to test Rails app code that relies on the presence of certain items in the database.

Figure 6.18 gives an overview of Jasmine for RSpec users. We will walk through five happy-path Jasmine specs for the popup-window functionality developed in Section 6.7. While these tests are hardly exhaustive even for the happy path, our goal is to illustrate Jasmine testing techniques generally and the use of Jasmine stubs and fixtures in AJAX testing specifically.

The basic structure of Jasmine test cases is immediately evident in Figure 6.20: like RSpec, Jasmine uses it to specify a single example and nestable describe blocks to group related sets of examples. Just as in RSpec, describe and it take a block of code as an argument, but whereas in Ruby code blocks are delimited by do...end, in JavaScript they are anonymous functions (functions without a name) of zero arguments. The punctuation sequence }); is so prevalent because describe and it are JavaScript functions of two arguments, the second of which is a function of no arguments.

The describe(’setup’) examples check that the MoviePopup.setup function correctly creates the #movieInfo container but keeps it hidden from display. toExist and toBeHidden are expectation matchers provided by the Jasmine-jQuery add-on. Since Jasmine loads all your JavaScript files before running any examples, the call to setup (line 34 of Figure 6.14) occurs before our tests run; hence it’s reasonable to test whether that function did its work.

6.18
Figure 6.18: A partial summary of a small subset of commonly used features in Jasmine and Jasmine-jQuery, following the structure of Figures 8.10 and 8.11 and extracted from the complete Jasmine documentation24 and Jasmine jQuery add-on25 documentation.
6.19
Figure 6.19: Continuation of Figure 6.18 describing stubs (spies in Jasmine) and fixtures.
 1describe('MoviePopup', function() {
 2    describe('setup', function() {
 3        it('adds popup Div to main page', function() {
 4        expect($('#movieInfo')).toExist();
 5        });
 6        it('hides the popup Div', function() {
 7        expect($('#movieInfo')).toBeHidden();
 8        });
 9    });
10    describe('clicking on movie link', function() {
11        beforeEach(function() { loadFixtures('movie_row.html'); });
12        it('calls correct URL', function() {
13        spyOn($, 'ajax');
14        $('#movies a').trigger('click');
15        expect($.ajax.calls.mostRecent().args[0]['url']).toEqual('/movies/1');
16        });
17        describe('when successful server call', function() {
18        beforeEach(function() {
19            let htmlResponse = readFixtures('movie_info.html');
20            spyOn($, 'ajax').and.callFake(function(ajaxArgs) {
21            ajaxArgs.success(htmlResponse, '200');
22            });
23            $('#movies a').trigger('click');
24        });
25        it('makes #movieInfo visible', function() {
26            expect($('#movieInfo')).toBeVisible();
27        });
28        it('places movie title in #movieInfo', function() {
29            expect($('#movieInfo').text()).toContain('Casablanca');
30        });
31        });
32    });
33});
Figure 6.20: Five happy-path Jasmine specs for the AJAX code developed in Section 6.7. Lines 2–9 check whether the MoviePopup.setup function correctly sets up the floating div that will be used to display movie info. Lines 10–32 check the behavior of the AJAX code without actually calling the RottenPotatoes server by stubbing around the AJAX call.
<div id="movies">
    <div class="row">
        <div class="col-8"><a href="/movies/1">Casablanca</a></div>
        <div class="col-2">PG</div>
        <div class="col-2">1943-01-23</div>
    </div>
</div>
Figure 6.21: This HTML fixture mimics a row of the #movies table generated by the RottenPotatoes list-of-movies view (Figure 4.5). Note that we omit the table header from the fixture, since the spec doesn’t require it to be present. This fixture would go in spec/javascripts/fixtures/movie_row.html. You can generate such fixtures by copy-and-pasting HTML code from “View Source” in the browser, or for source that was generated dynamically by JavaScript (such as the “Hide adult movies” checkbox), by inspecting $(’#movieInfo’).html() in the JavaScript console. Fallacies and Pitfalls describes a way to prevent such fixtures from getting out of sync if you change your app’s views.
6.22
Figure 6.22: Top: Normally, our getMovieInfo function calls jQuery’s ajax, which calls xhr in the browser’s JSAPI, which sends the request to the server. The server’s reply triggers callback logic in the browser’s JSAPI, which calls an internal jQuery method that eventually calls our showMovieInfo callback. If we stub the ajax function, we can cause showMovieInfo to be called immediately; we can also stub “farther away” by stubbing xhr (using the Jasmine-Ajax plugin), causing the jQuery internal dispatcher to be called immediately. Bottom: Graphical representation of the discussion in Section 8.4.

The describe( ’AJAX call to server’ ) examples are more interesting because they use stubs and fixtures to isolate our client-side AJAX code from the server with which it communicates. Figure 6.19 summarizes the stubs and fixtures available in Jasmine and Jasmine-jQuery. Like RSpec, Jasmine allows us to run test setup and teardown code using beforeEach and afterEach. In this set of examples, our setup code loads the HTML fixture shown in Figure 6.21, to mimic the environment the getMovieInfo handler would see if it was called after movie list was displayed. The fixtures functionality is provided by Jasmine-jQuery; each fixture is loaded inside of div#jasmine-fixtures, which is inside of div#jasmine_content on the main Jasmine page, and all the fixtures are cleared out after each spec to preserve test independence.

The first example (line 12 of Figure 6.20) checks that the AJAX call uses the correct movie URL derived from the table. To do this, it uses Jasmine’s spyOn to stub out the $.ajax function. Like RSpec’s stub, this call replaces any existing function of the same name, so when we manually trigger the click action on the (only) a element in the #movies table, if all is working well we should expect our spy function to have been called. Because in JavaScript it’s common for functions to be the values of object properties, spyOn takes two arguments, an object ($) and the name of the function-valued property of that object on which to spy (’ajax’).

Line 15 looks complex, but it’s straightforward. Each Jasmine spy remembers the argu- ments passed to it in each of its calls, e.g. calls.mostRecent(), and as you recall from the explanation in Section 6.7, a real call to the AJAX function takes a single object (lines 9–15 of Figure 6.14) whose url property is the URL to which the AJAX call should go. Line 15 of the spec is simply checking the value of this URL. In effect, it’s testing whether $(this).attr(’href’) is the correct JavaScript code to extract the AJAX URL from the table.

Figure 6.22 shows the similarity between the challenges of stubbing the Internet for testing AJAX and stubbing the Internet for testing code in a Service-Oriented Architecture (Section 8.4). As you can see, in both scenarios, the decision of where to stub depends on how much of the stack we want to exercise in our tests.

<p>Casablanca is a classic and iconic film starring
    Humphrey Bogart and Ingrid Bergman.</p>
<a href="" id="closeLink">Close </a>
describe('element sanitizer', function() {
    it('removes IMG tags from evil HTML', function() {
        setFixtures(sandbox({class: 'myTestClass'}));
        $('.myTestClass').text("Evil HTML! <img src='http://evil.com/xss'>");
        $('.myTestClass').sanitize();
        expect($('.myTestClass').text()).not.toContain('<img');
    });
});

Line 19 reads in a fixture that will take the place of the ajax response from the movies controller show action, see Figure 6.23. In lines 20–22 we see the use fo the callFake function to not only intercept an AJAX call, but also to fake a successful response using the fixture. This and the triggering of the AJAX call (line 23) is repeated for each of the following two tests which check that both the #movieInfo popup is visible (line 26) and that it contains text from the movie description (line 29).

This concise introduction, along with the summary tables in this section, should get you started using BDD for your JavaScript code. The best sources of complete documentation for these tools are the Jasmine documentation and the Jasmine jQuery add-on documentation.

Self-Check 6.8.1. Jasmine-jQuery also supports toContain and toContainText to check if a string of text or HTML occurs within an element. In line 7 of Figure 6.20, why would it be incorrect to substitute .not.toContain(’<div id="movieInfo"></div>’) for toBeHidden() ?

A hidden element is not visible, but it still contains the text or HTML associated with the element. Hence toContain -style matchers can be used to test the content of an element but not its visibility. In addition, there are many ways for an element to be hidden—its CSS could include display:none , it could have zero width and height, or its ancestor could be hidden—and the toBeHidden() matcher checks all of these.

Self-Check 6.8.2. Like RSpec, Jasmine supports and.returnValue() for returning a canned value from a stub. In Figure 6.20, why why did we have to write and.callFake to pass ajaxArgs to a function as the result of stubbing ajax, rather than simply writing and.returnValue(ajaxArgs) ?

Remember that AJAX calls are asynchronous. It’s not the case that the $.ajax call returns data from the server: normally, it returns immediately, and sometime later, your callback is called with the data from from the server. and.callFake simulates this behavior.