JS Unit Testing

Everything you always wanted to know about JS unit testing but were afraid to ask

Created by Carlos Villuedas / @carlosvillu

Main points

  • Tools
  • Principles
  • Techniques

Tools

Test frameworks

Test runners

Supporting tools

  • ChaiJS
  • SinonJS

Spy


"test should call subscribers on publish": function () {
  var callback = sinon.spy();
  PubSub.subscribe("message", callback);

  PubSub.publishSync("message");

  assertTrue(callback.called);
};
            

Stub


"test should call all subscribers, even if there are exceptions" : function(){
    var message = 'an example message';
    var error = 'an example error message';
    var stub = sinon.stub().throws();
    var spy1 = sinon.spy();
    var spy2 = sinon.spy();

    PubSub.subscribe(message, stub);
    PubSub.subscribe(message, spy1);
    PubSub.subscribe(message, spy2);

    PubSub.publishSync(message, undefined);

    assert(spy1.called);
    assert(spy2.called);
    assert(stub.calledBefore(spy1));
}
            

Mock


"test should call all subscribers when exceptions": function () {
    var myAPI = { method: function () {} };

    var spy = sinon.spy();
    var mock = sinon.mock(myAPI);
    mock.expects("method").once().throws();

    PubSub.subscribe("message", myAPI.method);
    PubSub.subscribe("message", spy);
    PubSub.publishSync("message", undefined);

    mock.verify();
    assert(spy.calledOnce);
}
            

Principles

Use clear, understandable test names


describe( '[unit of work]', function ()
{
    describe( 'when [scenario]', function ()
    {
        it( 'should [expected behaviour]', function ()
        {

        } );
    } );
} );
            

Test one thing at a time


it( 'should send the profile data to the server', function ()
{
    // expect(...)to(...);
} );

it( 'should update the profile view properly', function ()
{
    // expect(...)to(...);
} );
            

Test generic uses and corner cases


describe( 'The RPN expression evaluator', function ()
{
    it( 'should return null when the expression is an empty string', function ()
    {
        var result = RPN( '' );
        expect( result ).toBeNull();
    } );

    it( 'should return the same value when the expression holds a single value', function ()
    {
        var result = RPN( '42' );
        expect( result ).toBe( 42 );
    } );

    it( 'should properly calculate an expression', function ()
    {
        var result = RPN( '5 1 2 + 4 * - 10 /' );
        expect( result ).toBe( -0.7 );
    } );

    it( 'should throw an error whenever an invalid expression is passed', function ()
    {
        var compute = function ()
        {
            RPN( '1 + - 1' );
        };

        expect( compute ).toThrow();
    } );
} );
            

Use factories


function createProfileModule( options )
{
    return new ProfileModule( options || { views: 0 } );
}

describe( 'User profile module', function ()
{
    it( 'should return the current views count', function ()
    {
        var profileModule = createProfileModule( { views: 3 } );
        expect( profileModule.getViewsCount() ).toBe( 3 );
    } );

    it( 'should increase the views count properly', function ()
    {
        var profileModule = createProfileModule( { views: 41 } );
        profileModule.incViewsCount();
        expect( profileModule.getViewsCount() ).toBe( 42 );
    } );
} );            
            

Use fake objects

Fake timers


{
    setUp: function () {
        this.clock = sinon.useFakeTimers();
    },

    tearDown: function () {
        this.clock.restore();
    },

    "test should animate element over 500ms" : function(){
        var el = jQuery("
"); el.appendTo(document.body); el.animate({ height: "200px", width: "200px" }); this.clock.tick(510); assertEquals("200px", el.css("height")); assertEquals("200px", el.css("width")); } }

Fake AJAX


{
    setUp: function () {
        this.xhr = sinon.useFakeXMLHttpRequest();
        var requests = this.requests = [];

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

    tearDown: function () {
        this.xhr.restore();
    },

    "test should fetch comments from server" : function () {
        var callback = sinon.spy();
        myLib.getCommentsFor("/some/article", callback);
        assertEquals(1, this.requests.length);

        this.requests[0].respond(200, { "Content-Type": "application/json" },
                                 '[{ "id": 12, "comment": "Hey there" }]');
        assert(callback.calledWith([{ id: 12, comment: "Hey there" }]));
    }
}            

Fake server


{
    setUp: function () {
        this.server = sinon.fakeServer.create();
    },

    tearDown: function () {
        this.server.restore();
    },

    "test should fetch comments from server" : function () {
        this.server.respondWith("GET", "/some/article/comments.json",
            [200, { "Content-Type": "application/json" },
             '[{ "id": 12, "comment": "Hey there" }]']);

        var callback = sinon.spy();
        myLib.getCommentsFor("/some/article", callback);
        this.server.respond();

        sinon.assert.calledWith(callback, [{ id: 12, comment: "Hey there" }]);
    }
}          

Techniques

Practical examples

Questions?!