Testing Javascript Applications

by Dzmitry Herasimov

"Any feature without a test doesn’t exist"

Steve Loughran HP Laboratories

¿Por qué?

¡Porque!

  • refactor the code;
  • organize & modularize;
  • provide secondary documentation;
  • prevent defects;
  • collaborate;

Tests help you to:

Testing + CI = ❤

  • fail builds

  • statistics & reporting

  • Github is integration paradise

Tests

Some boring theory

Test types

Unit

Component/Snapshot

Integration Tests

e2e Tests

Security tests

Accessibility tests

The Test Pyramid

Unit tests

Integration tests

UI tests

More

integration

More

isolation

Slow

Fast

  • Setup: Put the Unit Under Test (UUT) or the overall test system in the state needed to run the test.
  • Execution: Trigger/drive the UUT to perform the target behavior and capture all output, such as return values and output parameters.
  • Validation: Ensure the results of the test are correct.
  • Cleanup: Restore the UUT or the overall test system to the pre-test state.

Structure

http://en.wikipedia.org/wiki/Test-driven_development#Test_structure

Tests F.I.R.S.T.

 

NOT! just First

F - Fast

  • Test, including setup and tear down,  should execute really fast (ms) .
  • A developer should not hesitate to run the tests as they are slow.

50ms

51ms

100ms

I - Isolated/Independent

  • Arrange: The data used in a test should not depend on the environment. All the data needed for a test should be arranged as part of the test.
  • Act: Invoke the actual method under test.
  • Assert: A test method should test for a single logical outcome.
  • Avoid doing asserts in the Arrange part, let it throw exceptions and your test will still fail.
  • No order-of-run dependency. They should pass or fail the same way in suite or when run individually.
  • Do not do any more actions after the assert statement(s), preferably single logical assert.

R - Repetable

  • Deterministic results - should yield the same results every time and at every location where they run.
  • No dependency on date/time or random functions output.
  • Each test should setup or arrange it's own data.
  • Use Data Helper classes that can setup this data for re-usability.

Self-Validating

  • No manual inspection required to check whether the test has passed or failed.

Thorough and Timely

  • Should cover every use case scenario and NOT just aim for 100% coverage.
  • Should try to aim for Test Driven Development (TDD) so that code does not need re-factoring later.

Test driven development

TDD

now really first

Tests first

Red

Green

Refactor

Red: Write failing tests

Red

Green

Refactor

Green: Make it pass

Red

Green

Refactor

Refactor: Eliminate redundancy

Behaviour Driven Development

BDD

Deliver software that matters

BDD

  • Where to start in the process?

  • What to test and what not to test?

  • How much to test in one go?

  • What to call the tests?

  • How to understand why a test fails?

"From the heavens to the depths"

Behavioral specifications

User story:
   As a [role] I want [feature] so that [benefit].
Acceptance criteria:
   Given [initial context].
   When [event occurs].
   Then [ensure some outcomes].

Example

Feature: Addition
  In order to avoid silly mistakes
  As a math idiot
  I want to be told the sum of two number

Scenario: Add two numbers
  Given I have entered 50 into the calculator
  And I have entered 70 into the calculator
  When I press add
  Then the result should be 120 on the screen

Techniques

Dummies

  • Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.

Stubs

  • Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

Mocks

  • A mock is something that as part of your test you have to setup with your expectations. A mock is not setup in a predetermined way so you have code that does it in your test. Mocks in a way are determined at runtime since the code that sets the expectations has to run before they do anything.

Spies

Fixtures

...finally... bothered!

Unit Tests

Today sponsors

  • Jasmine

  • Mocha ['mɔkə]

  • Chai

  • Sinon

Not suits

Suites & Specs

// Suite
describe("<unit or class name here>", function() {
  // Some variables and hooks* for test suite

  describe("#<method or test item name here>", function() {
    // Spec (your test)
    it("<behavior and result here>", function() {
      /*
      Initalization
      Actions
      Assertion
      */
    });
  });
});

Common test

describe('Array', function(){
  describe('#indexOf()', function(){
    it('should return -1 when the value is not present', function(){
      [1,2,3].indexOf(5).should.equal(-1);
    })
  })
})

What? Where? How many?

Assertions

Classic

Assert

var assert = chai.assert;

assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);

BDD style

Expect

var expect = chai.expect;

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(tea).to
           .have
           .property('flavors')
           .with.length(3);

even more BDD

Should

chai.should();

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
tea.should.have.property('flavors')
                .with.length(3);

not meat-hook!

Hooks

beforeEach(function(done){
db.clear(function(err){
  if (err) return done(err);
    db.save([tobi, loki, jane], done);
  });
})

['mɔkə]

Mocha

features

Mocha

  • Supports TDD assertions and BDD should/expect

  • Reporting & CI integration

  • Browser Test Runner

Async test

describe('User', function(){
  describe('#save()', function(){
    it('should save without error', function(done){
      var user = new User('Luna');
      user.save(function(err){
        if (err) throw err;
        done();
      });
    })
  })
})

Hooks: before(), after(), beforeEach(), afterEach()

Hooks

beforeEach(function(done){
db.clear(function(err){
  if (err) return done(err);
    db.save([tobi, loki, jane], done);
  });
})

Reporters

Jasmine

What is it?

Matchers

expect(x).toEqual(y);
expect(x).toBe(y);
expect(x).toMatch(pattern);
expect(x).toBeDefined();
expect(x).toBeUndefined();
expect(x).toBeNull();
expect(x).toBeTruthy();
expect(x).toBeFalsy();
expect(x).toContain(y);
expect(x).toBeLessThan(y);
expect(x).toBeGreaterThan(y);
expect(function(){fn();}).toThrow(e);

Custom matcher

var customMatchers = {
  toBeGoofy: function(util, customEqualityTesters) {
    return {
      compare: function(actual, expected) {
        if (expected === undefined) {
          expected = '';
        }

        var result = {};
        result.pass = util.equals(actual.hyuk, "gawrsh" + expected, customEqualityTesters);

        if (result.pass) {
          result.message = "Expected " + actual + " not to be quite so goofy";
        } else {
          result.message = "Expected " + actual + " to be goofy, but it was not very goofy";
        }

        return result;
      }
    };
  }
};

Spies

spyOn(obj, 'method');
expect(obj.method).toHaveBeenCalled();
expect(obj.method).toHaveBeenCalledWith('foo', 'bar')
obj.method.callCount
obj.method.mostRecentCall.args
obj.method.reset()
spyOn(obj, 'method').andCallThrough()
obj.method.argsForCall
spyOn(obj, 'method').andReturn('Pow!')

Any

describe("jasmine.any", function() {
  it("matches any value", function() {
    expect({}).toEqual(jasmine.any(Object));
    expect(12).toEqual(jasmine.any(Number));
  });
});

Clock

beforeEach(function() {
    timerCallback = jasmine.createSpy("timerCallback"); //create spy
    jasmine.Clock.useMock(); //use wrapper of system timer
});

it("causes a timeout to be called synchronously", function() {
    setTimeout(function() {
      timerCallback();
    }, 100);

    expect(timerCallback).not.toHaveBeenCalled();
    jasmine.Clock.tick(101); //make time go
    expect(timerCallback).toHaveBeenCalled();
});

Chai.JS

Chai

chai.should();

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors')
  .with.lengthOf(3);
var expect = chai.expect;

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property('flavors')
  .with.lengthOf(3);
var assert = chai.assert;

assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);

Question Time!

How would you test RNG?

Plugins

Chai

Sinon.js

  • Standalone test spies, stubs and mocks for JavaScript.
  • No dependencies, works with any unit testing framework.

Spies

it("calls original function with right this and args", function () {
    var callback = sinon.spy();
    var proxy = once(callback);
    var obj = {};

    proxy.call(obj, 1, 2, 3);

    assert(callback.calledOn(obj));
    assert(callback.calledWith(1, 2, 3));
});

Stubs

it("returns the value from the original function", function () {
    var callback = sinon.stub().returns(42);
    var proxy = once(callback);

    assert.equals(proxy(), 42);
});

Ajax

function getTodos(listId, callback) {
    jQuery.ajax({
        url: "/todo/" + listId + "/items",
        success: function (data) {
            // Node-style CPS: callback(err, data)
            callback(null, data);
        }
    });
}

Ajax

after(function () {
    // When the test either fails or passes, restore the original
    // jQuery ajax function (Sinon.JS also provides tools to help
    // test frameworks automate clean-up like this)
    jQuery.ajax.restore();
});

it("makes a GET request for todo items", function () {
    sinon.stub(jQuery, "ajax");
    getTodos(42, sinon.spy());

    assert(jQuery.ajax.calledWithMatch({ url: "/todo/42/items" }));
});

Fake XMLHttpRequest

var xhr, requests;

before(function () {
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function (req) { requests.push(req); };
});

after(function () {
    // Like before we must clean up when tampering with globals.
    xhr.restore();
});

it("makes a GET request for todo items", function () {
    getTodos(42, sinon.spy());

    assert.equals(requests.length, 1);
    assert.match(requests[0].url, "/todo/42/items");
});

Fake server

var server;

before(function () { server = sinon.fakeServer.create(); });
after(function () { server.restore(); });

it("calls callback with deserialized data", function () {
    var callback = sinon.spy();
    getTodos(42, callback);

    // This is part of the FakeXMLHttpRequest API
    server.requests[0].respond(
        200,
        { "Content-Type": "application/json" },
        JSON.stringify([{ id: 1, text: "Provide examples", done: true }])
    );

    assert(callback.calledOnce);
});

Fake time

function throttle(callback) {
    var timer;

    return function () {
        var args = [].slice.call(arguments);

        clearTimeout(timer);
        timer = setTimeout(function () {
            callback.apply(this, args);
        }, 100);
    };
}

Fake time

var clock;

before(function () { clock = sinon.useFakeTimers(); });
after(function () { clock.restore(); });

it("calls callback after 100ms", function () {
    var callback = sinon.spy();
    var throttled = throttle(callback);

    throttled();

    clock.tick(99);
    assert(callback.notCalled);

    clock.tick(1);
    assert(callback.calledOnce);
}

Coding time!

The End

Testing Javascript Applications

By Timon Thelure

Testing Javascript Applications

  • 700