Using QUnit to test JavaScript Callbacks

Written by: Clemens Helm

https://fast.wistia.com/embed/medias/5mln9c0d97.jsonphttps://fast.wistia.com/assets/external/E-v1.js

This is the 21st and – for now – last Testing Tuesday episode of this season 1. Every week we shared our insights and opinions on the software testing space. Last week we continuously deployed a node.js application to Heroku with Codeship.


How to test JavaScript callbacks with QUnit

QUnit is a JavaScript unit testing framework. It is used to test projects like jQuery, jQuery UI and jQuery Mobile. It has got strong roots in frontend JavaScript testing and includes some neat features to test DOM operations. That's why we're using it for frontend testing in this screencast. QUnit is perfectly capable of testing your node.js projects as well, though.

Synchronous callbacks

How do you test that a callback function gets called after you pass it to another function? You can pass an anonymous function containing an assertion as a callback. But then the problem is, even if this assertion never gets executed, the test won't fail because all other assertions passed. In QUnit, you can tell a test how many assertions should succeed. This way the test will fail if some assertions don't get executed.

Asynchronous callbacks

But what if a callback is only invoked some time later, after a result came in or after some time passed? The test will probably have finished unsuccessfully by then. QUnit allows tests to wait for asynchronous callbacks to be called.

The screencast contains examples of both, synchronous and asynchronous callbacks. Watch the screencast to learn more!

Further info:

Following up: Efficiency in Development Workflows series

Transcript

TT21 – Testing synchronous and asynchronous JavaScript callbacks with QUnit

Intro

Ahoy and welcome! My name is Clemes Helm and you're watching Codeship's Testing Tuesday episode 21. Today is the last Testing Tuesday for this season and we're gonna finish our focus on JavaScript by testing synchronous and asynchronous callback functions with QUnit.

Screencast

QUnit is a JavaScript unit testing framework. It is used to test projects like jQuery, jQuery UI and jQuery Mobile. It has got strong roots in frontend JavaScript testing and includes some neat features to test DOM operations. That's why we're also gonna use it for frontend testing in this screencast. QUnit is perfectly capable of testing your node.js projects as well, though.

We're building the frontend of an e-commerce application today. So far we've only got the most important UI element: the checkout button.

<!DOCTYPE HTML>
<html lang="en-EN">
<head>
  <meta charset="UTF-8">
  http://jquery-2.0.3.js
  http://app.js
  <title></title>
</head>
<body>
  <a href="/checkout" id="checkout_button">Checkout</a>
</body>
</html>

We've got a Javascript file that contains a module with 2 functions that deal with the checkout button. Let's write some tests for this functions with QUnit to make sure they work. The first function triggers a callback when the checkout button is clicked. In QUnit we can write a test for it like this:

test("it triggers the checkout callback", function () {
  var $fixture = $("#qunit-fixture");
  $fixture.append('<a id="checkout_button">Checkout</a>');
  app.onCheckout(function () {
    ok(true, "Checkout callback was called.");
  });
  var $checkoutButton = $("#checkout_button");
  equal($checkoutButton.length, 1, "Checkout button exists");
  $checkoutButton.click();
});

To create a test, we call the test function and pass it a description of the text and an anonymous function containing the test code. We insert a checkout button into our test environment. Then we set an onCheckout callback that makes sure it was called.

Afterwards we validate that our checkout button exists by asserting that the jQuery selector matches exactly one element. The texts we add at the end to the ok and the equal functions describe what the assertions do and will also show up in our test results.

To run our test, we open the file test.html in the browser. We can see that our test works! Great! When we open the test, it shows us the descriptions of the assertions that ran. But why is there only one? Apparently our callback assertion wasn't performed. But why did the test succeed then? QUnit doesn't know that there should be 2 assertions in this test. It's happy that there was one assertion that passed, therefore it let the test succeed.

Fortunately we can tell QUnit to expect 2 assertions:

test("it triggers the checkout callback", function () {
  expect(2);
  var $fixture = $("#qunit-fixture");
  $fixture.append('<a id="checkout_button">Checkout</a>');
  app.onCheckout(function () {
    ok(true, "Checkout callback was called.");
  });
  var $checkoutButton = $("#checkout_button");
  equal($checkoutButton.length, 1, "Checkout button exists");
  $checkoutButton.click();
});

When we run the tests now, the test fails, telling us that it "Expected 2 assertions, but 1 were run".

Our second assertion wasn't run because callbacks are only called on active checkout buttons. So let's add a class active to our fixture.

By the way, what's this #qunit-fixture element? If we look into our test.html file, there's one element with id qunit which will contain the test results and one element qunit-fixtures which is meant for test data. The qunit-fixtures element is special in that QUnit will clear it after each test. This way we'll always have a clean fixtures element. If we put our fixtures somewhere else, we'd have to clean them after each test manually to provide the same conditions for all our tests.

But back to the example. We added the class active to our fixture, now let's run the test again. This time it works and it executed both assertions.

There is one more function to test. The delayedCheckoutActivation activates the checkout button after one second to prevent users from clicking it accidentally before. It adds a class active to the button and triggers an activation event afterwards.

Let's test this function:

test("the checkout button becomes active after some time", function () {
  var $fixture = $("#qunit-fixture");
  $fixture.append('<a id="checkout_button">Checkout</a>');
  app.delayedCheckoutActivation();
  var $checkoutButton = $("#checkout_button");
  $checkoutButton.on("activation", function () {
    equal($checkoutButton.hasClass("active"), true, "Checkout button is active");
  });
});

So we've got a test again that inserts a checkout button into the fixtures and calls the delayedCheckoutActivation. Then it checks if the active class has been set as soon as the activation event was triggered.

This test fails stating "Expected at least one assertion, but none were run". The reason is that the test didn't wait for the event to be called but exited immediately after adding the event callback. QUnit offers a solution for such asynchronous callbacks as well: Instead of a test we create an asyncTest. This will pause at the end and wait for a call to the start() function. So let's call start after our asynchronous assertion:

asyncTest("the checkout button becomes active after some time", function () {
  var $fixture = $("#qunit-fixture");
  $fixture.append('<a id="checkout_button">Checkout</a>');
  app.delayedCheckoutActivation();
  var $checkoutButton = $("#checkout_button");
  $checkoutButton.on("activation", function () {
    equal($checkoutButton.hasClass("active"), true, "Checkout button is active");
    start();
  });
});

Now our test passes. We can see that it takes one second to wait for the event to be triggered, but eventually it works. Great!

Outro

This was our first Testing Tuesday season, I hope you liked it! Please let me know what you liked most, what you didn't like and especially what you would like us to cover in the next season. In the meantime check out the other blog posts on the Codeship blog. We'll keep publishing a lot of interesting articles, so don't miss out on them. Thank you for watching these 21 episodes. And one last time: Always stay shipping!

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.