Unit Test your AJAX-tastic JavaScript with QUnit and Sinon JS – Why Test? – Writing Testable Code



Unit Test your AJAX-tastic JavaScript with QUnit and Sinon JS – Why Test? – Writing Testable Code

0 0


qunit-talk


On Github katbailey / qunit-talk

Unit Test your AJAX-tastic JavaScript with QUnit and Sinon JS

Presented by Kat Bailey / @katherinebailey

What we'll talk about

  • Why test?
  • Writing testable code
  • QUnit testing framework
  • Sinon JS for testing AJAX
  • Automation

Kudos

Big thanks to Jordan Kasper aka @jakerella whose slides on this topic I borrowed heavily from. See http://jordankasper.com/js-testing

Note

Although I focus here on QUnit and Sinon JS, there are other frameworks, e.g. Jasmine and jQuery Mockjax, that provide very similar functionality. The ideas are generally the same across all of these frameworks.

Why Test?

We write tests...

  • So that we are not scared to deploy our code
  • So that we can sleep at night

Writing Testable Code

Talk about the fact that we want a more unit-testable approach, but that integration testing can be very helpful as well.

Search Example


                    

What's so bad about that?

Anonymous Functions


                    

Does this look familiar?

Call Stack
                            (anonymous)         app.min.js 3
                            (anonymous)         app.min.js 3
                            (anonymous)         app.min.js 3
                            (anonymous)         app.min.js 3
In production we may have minified code, and be unable to see what's really going on. Even if it isn't minified, this can make for much easier diagnosis of a problem.

DOM Coupling



                        
                            Associating initialization with document onload means we can't test the initialization!
                        

DOM Coupling



                        
                            Tight coupling with the HTML makes testing outside of the context
                            of an entire page difficult.
                        

Server Coupling

(and asynchronous activity)

                    

Refactor Until It Hurts

(and then refactor a little more)

Discuss refactoring in chuncks, abstract one thing at a time

Search Example - Redux



                        
                            Be sure to mention namespacing and the named, separate functions.
                            Mention callback on doSearch()
                        

Now isn't that nice?

Call Stack
                            jk.handleResults       app.min.js 3
                            successHandler         app.min.js 3
                            jk.doSearch            app.min.js 3
                            submitHandler          app.min.js 3
                            jk.initSearch          app.min.js 3
                            initPage               index.html 376

Search Example - Initialization



                        


                        
                            Note that we can test this much easier now by passing in whatever
                            DOM nodes we want to test - loose coupling.

                            Also point out naming of inline functions.
                        

Inline != Anonymous


                    

Search Example - Redux



                        
                            Mention mocking ajax requests (will be mentioned later as well).
                        

Writing Testable Code

  • Use namespaces
  • Name your functions
  • Avoid tight coupling

Personalization Example

$(document).ready(function() {
    if (Drupal.settings.hasOwnProperty('personalization')) {
        var settings = Drupal.settings.personalization;
        var $option_set = $(settings.selector);
        $.ajax({
            'url': 'http://www.my-personalization-service.com/get-decision',
            'data': {'options': settings.options},
            'success': function(response) {
                var choice = response.data.choice;
                // [Various edge case logic...]
                var json = $('script[type="text/template"]', $option_set).get(0).innerText;
                var choices = jQuery.parseJSON(json);
                var chosen = choices[choice]['html'];
                $option_set.append(chosen);
            }
        });
    }
});
Drupal.personalize.showChoice = function($option_set, choice) {
    // [Various edge case logic...]
    var json = $('script[type="text/template"]', $option_set).get(0).innerText;
    var choices = jQuery.parseJSON(json);
    var chosen = choices[choice]['html'];
    $option_set.append(chosen);
};
$(document).ready(function() {
    if (Drupal.settings.hasOwnProperty('personalization')) {
        var settings = Drupal.settings.personalization;
        var $option_set = $(settings.selector);
        $.ajax({
            'url': 'http://www.my-personalization-service.com/get-decision',
            'data': {'options': settings.options},
            'success': function(response) {
                Drupal.personalize.showChoice($option_set, response.data.choice);
            }
        });
    }
});

Now we have a piece of testable code - how do we test it?

Introducing QUnit

  • A JavaScript unit testing framework
  • Used by jQuery and jQuery UI
  • Can test generic js code as well as (obvs) jQuery

QUnit test suite

The test suite corresponds to a single html file which loads QUnit and your tests to be run.

In general, use a different suite per area of functionality being tested.

tests.js

QUnit.test( "hello test", function( assert ) {
    assert.ok( 1 == "1", "Passed!" );
});
                        

Let's see an example!

Fixtures

QUnit will reset the elements inside the #qunit-fixture element after each test

Modules

All tests that occur after a call to QUnit.module() will be grouped into that module

Beyond the basics

What if my code relies on Drupal.settings?

QUnit.module("Tests that rely on some settings", {
  'setup': function() {
    Drupal.settings.personalize = {
      'option_sets': {
        'option-set-1': {
          'selector': '.some-class',
        }
      }
    };
  },
});

What if my code relies on other JS libraries?

Add them to your test suite

What if my code relies on other JS libraries?

Add them to your test suite

What if my code has asynchronous behaviors?

QUnit.async()

What if my code has asynchronous behaviors?

QUnit.async()

What if my code uses AJAX?

Glad you asked! :D

What if my code uses AJAX?

Glad you asked! :D

Sinon JS

.. and spies

Sinon JS

.. and spies

Spies, mocks, stubs, oh my!

Different types of test doubles that serve slightly different purposes

Spies, mocks, stubs, oh my!

Different types of test doubles that serve slightly different purposes

Sinon spies

A function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.

Sinon spies

A test spy can be an anonymous function or it can wrap an existing function.

Spy

// Create an anonyous spy.
sinon.spy();

// Spy on a particular function.
sinon.spy(myFunc);

// Spy on a method of an object.
sinon.spy(myObj, 'mymethod');

Spy

var myCallback = sinon.spy();

// Pass my spy to the function I'm testing.
myFuncThatShouldCallMyCallback(myCallback);

// Confirm my spy was called.
assert(callback.called);

See http://sinonjs.org/docs/#spies for more things you can do with spies.

Back to AJAX...

How do we test AJAX code without triggering network activity?

Use a fakeXMLHttpRequest :D

How do we test AJAX code without triggering network activity?

Use a fakeXMLHttpRequest :)

// Replace the native XMLHttpRequest object with Sinon's fake
var xhr = sinon.useFakeXMLHttpRequest();
var requests = [];

xhr.onCreate = function (request) {
  requests.push(request);
};

                        

Let's put it all together!

Yay - I haz tests!

Now what?

Automation

Grunt

Travis CI

A hosted continuous integration service

Phantom JS

A headless browser

Setting it up

  • Sign in to Travis with your Github account
  • Activate the github web hook on the project you want to add Travis integration on
  • Add .travis.yml to your repo
                            
language: php
php:
- 5.4
before_install:
- wget https://raw.githubusercontent.com/jonkemp/qunit-phantomjs-runner/master/runner.js
script:
- phantomjs runner.js qunit/test.html
                            
                        

Resources

Thanks!

Questions?