Chapter 13. Dipping Our Toes, Very Tentatively, into JavaScript

 

If the Good Lord had wanted us to enjoy ourselves, he wouldn’t have granted us his precious gift of relentless misery.

 
  -- John Calvin (as portrayed in Calvin and the Chipmunks)

Our new validation logic is good, but wouldn’t it be nice if the error messages disappeared once the user started fixing the problem? For that we’d need a teeny-tiny bit of JavaScript.

We are utterly spoiled by programming every day in such a joyful language as Python. JavaScript is our punishment. So let’s dip our toes in, very gingerly.

I’m going to assume you know the basics of JavaScript syntax. If you haven’t read JavaScript: The Good Parts, go and get yourself a copy right away! It’s not a very long book.

Starting with an FT

Let’s add a new functional test to the ItemValidationTest class:

functional_tests/test_list_item_validation.py (ch14l001). 

def test_error_messages_are_cleared_on_input(self):
    # Edith starts a new list in a way that causes a validation error:
    self.browser.get(self.server_url)
    self.get_item_input_box().send_keys('\n')
    error = self.browser.find_element_by_css_selector('.has-error')
    self.assertTrue(error.is_displayed()) #1

    # She starts typing in the input box to clear the error
    self.get_item_input_box().send_keys('a')

    # She is pleased to see that the error message disappears
    error = self.browser.find_element_by_css_selector('.has-error')
    self.assertFalse(error.is_displayed()) #2

1 2

is_displayed() tells you whether an element is visible or not. We can’t just rely on checking whether the element is present in the DOM, because now we’re starting to hide elements.

That fails appropriately, but before we move on: three strikes and refactor! We’ve got several places where we find the error element using CSS. Let’s move it to a helper function:

functional_tests/test_list_item_validation.py (ch14l002). 

    def get_error_element(self):
        return self.browser.find_element_by_css_selector('.has-error')

I like to keep helper functions in the FT class that’s using them, and only promote them to the base class when they’re actually needed elsewhere. It stops the base class from getting too cluttered. YAGNI.

And we then make five replacements in test_list_item_validation, like this one for example:

functional_tests/test_list_item_validation.py (ch14l003). 

    # She is pleased to see that the error message disappears
    error = self.get_error_element()
    self.assertFalse(error.is_displayed())

We have an expected failure:

$ python3 manage.py test functional_tests.test_list_item_validation
[...]
    self.assertFalse(error.is_displayed())
AssertionError: True is not false

And we can commit this as the first cut of our FT.

Setting Up a Basic JavaScript Test Runner

Choosing your testing tools in the Python and Django world is fairly straightforward. The standard library unittest package is perfectly adequate, and the Django test runner also makes a good default choice. There are some alternatives out there—nose is popular, Green is the new kid on the block, and I’ve personally found pytest to be very impressive. But there is a clear default option, and it’s just fine.[19]

Not so in the JavaScript world! We use YUI at work, but I thought I’d go out and see whether there were any new tools out there. I was overwhelmed with options—jsUnit, Qunit, Mocha, Chutzpah, Karma, Jasmine, and many more. And it doesn’t end there either: as I had almost settled on one of them, Mocha,[20] I find out that I now need to choose an assertion framework and a reporter, and maybe a mocking library, and it never ends!

In the end I decided we should use QUnit because it’s simple, and it works well with jQuery.

Make a directory called tests inside lists/static, and download the Qunit JavaScript and CSS files into it, stripping out version numbers if necessary (I got version 1.12). We’ll also put a file called tests.html in there:

$ tree lists/static/tests/
lists/static/tests/
├── qunit.css
├── qunit.js
└── tests.html

The boilerplate for a QUnit HTML file looks like this, including a smoke test:

lists/static/tests/tests.html. 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Javascript tests</title>
    <link rel="stylesheet" href="qunit.css">
</head>

<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script src="qunit.js"></script>
    <script>
/*global $, test, equal */

test("smoke test", function () {
    equal(1, 1, "Maths works!");
});

    </script>

</body>
</html>

Dissecting that, the important things to pick up are the fact that we pull in qunit.js using the first <script> tag, and then use the second one to write the main body of tests.

Are you wondering about the /*global comment? I’m using a tool called jslint, which is a syntax-checker for Javascript that’s integrated into my editor. The comment tells it what global variables are expected—it’s not important to the code, so don’t worry about it, but I would recommend taking a look at Javascript linters like jslint or jshint when you get a moment. They can be very useful for avoiding JavaScript "gotchas".

If you open up the file using your web browser (no need to run the dev server, just find the file on disk) you should see something like Figure 13-1.

Qunit screen showing 1 passing test
Figure 13-1. Basic QUnit screen

Looking at the test itself, we’ll find many similarities with the Python tests we’ve been writing so far:

test("smoke test", function () { // 1
    equal(1, 1, "Maths works!"); // 2
});

1

The test function defines a test case, a bit like def test_something(self) did in Python. Its first argument is a name for the test, and the second is a function for the body of the test.

2

The equal function is an assertion; very much like assertEqual, it compares two arguments. Unlike in Python, though, the message is displayed both for failures and for passes, so it should be phrased as a positive rather than a negative.

Why not try changing those arguments to see a deliberate failure?

Using jQuery and the Fixtures Div

Let’s get a bit more comfortable with what our testing framework can do, and start using a bit of jQuery

If you’ve never seen jQuery before, I’m going to try and explain it as we go, just enough so that you won’t be totally lost; but this isn’t a jQuery tutorial. You may find it helpful to spend an hour or two investigating jQuery at some point during this chapter.

Let’s add jQuery to our scripts, and a few elements to use in our tests:

lists/static/tests/tests.html. 

    <div id="qunit-fixture"></div>

    <form> 1
        <input name="text" />
        <div class="has-error">Error text</div>
    </form>

    <script src="http://code.jquery.com/jquery.min.js"></script>
    <script src="qunit.js"></script>
    <script>
/*global $, test, equal */

test("smoke test", function () {
    equal($('.has-error').is(':visible'), true); //23
    $('.has-error').hide(); //4
    equal($('.has-error').is(':visible'), false); //5
});

    </script>

1

The <form> and its contents are there to represent what will be on the real list page.

2

jQuery magic starts here! $ is the jQuery Swiss Army knife. It’s used to find bits of the DOM. Its first argument is a CSS selector; here, we’re telling it to find all elements that have the class "error". It returns an object that represents one or more DOM elements. That, in turn, has various useful methods that allow us to manipulate or find out about those elements.

3

One of which is .is, which can tell us whether an element matches a particular CSS property. Here we use :visible to check whether the element is displayed or hidden.

4

We then use jQuery’s .hide() method to hide the div. Behind the scenes, it dynamically sets a style="display: none" on the element.

5

And finally we check that it’s worked, with a second equal assertion.

If you refresh the browser, you should see that all passes:

Expected results from QUnit in the browser. 

2 assertions of 2 passed, 0 failed.
1. smoke test (0, 2, 2)

Time to see how fixtures work. Let’s just dupe up this test:

lists/static/tests/tests.html. 

    <script>
/*global $, test, equal */

test("smoke test", function () {
    equal($('.has-error').is(':visible'), true);
    $('.has-error').hide();
    equal($('.has-error').is(':visible'), false);
});
test("smoke test 2", function () {
    equal($('.has-error').is(':visible'), true);
    $('.has-error').hide();
    equal($('.has-error').is(':visible'), false);
});

    </script>

Slightly unexpectedly, we find one of them fails—see Figure 13-2.

Qunit screen showing only 1 passing test
Figure 13-2. One of the two tests is failing

What’s happening here is that the first test hides the error div, so when the second test runs, it starts out invisible.

QUnit tests do not run in a predictable order, so you can’t rely on the first test running before the second one.

We need some way of tidying up between tests, a bit like setUp and tearDown, or like the Django test runner would reset the database between each test. The qunit-fixture div is what we’re looking for. Move the form in there:

lists/static/tests/tests.html. 

    <div id="qunit"></div>
    <div id="qunit-fixture">
        <form>
            <input name="text" />
            <div class="has-error">Error text</div>
        </form>
    </div>

    <script src="http://code.jquery.com/jquery.min.js"></script>

As you’ve probably guessed, jQuery resets the content of the fixtures div before each test, so that gets us back to two neatly passing tests:

4 assertions of 4 passed, 0 failed.
1. smoke test (0, 2, 2)
2. smoke test 2 (0, 2, 2)

Building a JavaScript Unit Test for Our Desired Functionality

Now that we’re acquainted with our JavaScript testing tools, we can switch back to just one test, and start to write the real thing:

lists/static/tests/tests.html. 

    <script>
/*global $, test, equal */

test("errors should be hidden on keypress", function () {
    $('input').trigger('keypress'); // 1
    equal($('.has-error').is(':visible'), false);
});

    </script>

1

The jQuery .trigger method is mainly used for testing. It says "fire off a JavScript DOM event on the element(s)". Here we use the keypress event, which is fired off by the browser behind the scenes whenever a user types something into a particular input element.

jQuery is hiding a lot of complexity behind the scenes here. Check out Quirksmode.org for a view on the hideous nest of differences between the different browsers' interpretation of events. The reason that jQuery is so popular is that it just makes all this stuff go away.

And that gives us:

0 assertions of 1 passed, 1 failed.
1. errors should be hidden on keypress (1, 0, 1)
    1. failed
        Expected: false
        Result: true

Let’s say we want to keep our code in a standalone JavaScript file called list.js.

lists/static/tests/tests.html. 

    <script src="qunit.js"></script>
    <script src="../list.js"></script>
    <script>

Here’s the minimal code to get that test to pass:

lists/static/list.js. 

$('.has-error').hide();

It has an obvious problem. We’d better add another test:

lists/static/tests/tests.html. 

test("errors should be hidden on keypress", function () {
    $('input').trigger('keypress');
    equal($('.has-error').is(':visible'), false);
});

test("errors not be hidden unless there is a keypress", function () {
    equal($('.has-error').is(':visible'), true);
});

Now we get an expected failure:

1 assertions of 2 passed, 1 failed.
1. errors should be hidden on keypress (0, 1, 1)
2. errors not be hidden unless there is a keypress (1, 0, 1)
    1. failed
        Expected: true
        Result: false
        Diff: true false
[...]

And we can make a more realistic implementation:

lists/static/list.js. 

$('input').on('keypress', function () { //1
    $('.has-error').hide();
});

1

This line says: find all the input elements, and for each of them, attach an event listener which reacts on keypress events. The event listener is the inline function, which hides all elements that have the class .has-error.

That gets our unit tests to pass:

2 assertions of 2 passed, 0 failed.

Grand, so let’s pull in our script, and jQuery, on all our pages:

lists/templates/base.html (ch14l014). 

</div>
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="/static/list.js"></script>
</body>

</html>

It’s good practice to put your script-loads at the end of your body HTML, as it means the user doesn’t have to wait for all your JavaScript to load before they can see something on the page. It also helps to make sure most of the DOM has loaded before any scripts run.

Aaaand we run our FT:

$ python3 manage.py test functional_tests.test_list_item_validation.\
ItemValidationTest.test_error_messages_are_cleared_on_input
[...]

Ran 1 test in 3.023s

OK

Hooray! That’s a commit!

Javascript Testing in the TDD Cycle

You may be wondering how these JavaScript tests fit in with our "double loop" TDD cycle. The answer is that they play exactly the same role as our Python unit tests.

  1. Write an FT and see it fail.
  2. Figure out what kind of code you need next: Python or JavaScript?
  3. Write a unit test in either language, and see it fail.
  4. Write some code in either language, and make the test pass.
  5. Rinse and repeat.

Want a little more practice with JavaScript? See if you can get our error messages to be hidden when the user clicks inside the input element, as well as just when they type in it. You should be able to FT it too.

Columbo Says: Onload Boilerplate and Namespacing

Oh, and one last thing. Whenever you have some JavaScript that interacts with the DOM, it’s always good to wrap it in some "onload" boilerplate code to make sure that the page has fully loaded before it tries to do anything. Currently it works anyway, because we’ve placed the <script> tag right at the bottom of the page, but we shouldn’t rely on that.

The jQuery onload boilerplate is quite minimal:

lists/static/list.js. 

$(document).ready(function () {
    $('input').on('keypress', function () {
        $('.has-error').hide();
    });
});

In addition, we’re using the magic $ function from jQuery, but sometimes other JavaScript libraries try and use that too. It’s just an alias for the less contested name jQuery though, so here’s the standard way of getting more fine-grained control over the namespacing:

lists/static/list.js. 

jQuery(document).ready(function ($) {
    $('input').on('keypress', function () {
        $('.has-error').hide();
    });
});

Read more in the jQuery .ready() docs.

We’re almost ready to move on to Part III. The last step is to deploy our new code to our servers.

A Few Things That Didn’t Make It

  • The selector $(input) is way too greedy; it’s assigning a handler to every input element on the page. Try the exercise to add a click handler and you’ll realise why that’s a problem. Make it more discerning!
  • On a related note, we’re currently relying on lists.js binding listeners to whatever it finds in the DOM when it’s loaded, which means any elements that are added dynamically will not have them. You’ll find this is a problem if you do do the onclick exercise, and you’ll need to work around it. You could use an initialisation function and call it in each test, or find out about the jQuery .on delegation syntax[21]
  • At the moment our test only checks that the JavaScript works on one page. It works because we’re including it in base.html, but if we’d only added it to home.html the tests would still pass. It’s a judgement call, but you could choose to write an extra test here.
  • The new shiny thing in the world of front-end development are MVC frameworks like angular.js. Most tutorials for Angular use a test runner called Karma, and an RSpec-like assertion library called Jasmine. If you’re going to use angular, you’ll probably find life easier if you use those rather than Qunit.


[19] Admittedly once you start looking for Python BDD tools, things are a little more confusing.

[20] Purely because it features the NyanCat test runner.

[21] thanks to Vincenzo P. for pointing that one out!