Unit Testing

The perception...

Cumbersome?

Complicated?

Convoluted?

The reality?

Well supported

Tonnes of tooling

Automation is easy

...and if you know JavaScript already have the skills to do it!

"SEEMPLES"!

Tooling & Services

(You probably already know some!)

Jasmine/Mocha

 

  • Test frameworks
  • Scaffolding for tests
  • Provide output and reports
  • Support async/sync tests
  • Support for Node.js and Browsers

Chai

 

  • Expressive assertions
  • Easy to read due to expressiveness
  • Easy to write due to expressiveness
  • Multiple assertion styles
  • Support for Node.js and Browser

PhantomJS

  • Headless WebKit browser
  • Run tests without a browser window
  • JavaScript API for browser control
  • Simple to use with any framework

Karma

 

  • The glue for client-side tests
  • Run tests on real browsers
  • Framework agnostic
  • Automatically run tests when files are updated
  • Headless browser support e.g PhantomJS
  • Use via CLI or JavaScript

Testling CI

 

  • Free CI for Open Source projects
  • Great for Node.js style modules
  • Easy to use; just update package.json
  • Links with GitHub repositories
  • Test code across many browsers
  • Gives you a shiny badge of results
  • Private repositories are not free 

Travis CI

  • Free CI for open source
  • Paid plans for private repos
  • Supports many languages
  • Can be used for JS client-side testing with PhantomJS
  • Requires adding a .travis.yml to the project root
  • Can be used for Node.js testing

Jasmine/Mocha

Install

$npm install -g mocha

or

$npm install -g jasmine

"describe"

  • Describe "what" you're testing
  • Can be nested for structure
  • Nested describes are nested in output results
  • Assertions do not belong here!

Nested Describes

"it"

  • Describes what "should" happen
  • Any assertions being made go in here
  • If an assertion fails, this "it"/test case fails
  • Cannot be nested like "describe"
'use strict';

var drinks = require('drinks'); // or window.drinks in a browser

describe('Drinks Module', function () {

    describe('#getSomethingStrong', function () {
        it('Should return "whiskey"', function () {
            var res = drinks.getSomethingStrong();
            expect(res).toEqual('whiskey');
        });
    });

    describe('#getLemonade', function () {
        it('Should return lemonade', function () {
            var res = drinks.getSomethingStrong();
            expect(res).toEqual('lemonade');
        });
    });
})

Sample Code

Mocha

  • Let's you choose assertion libraries
  • Less mature than Jasmine
  • Simple async syntax
  • Support BDD and TDD
  • CLI can setup testing env

Jasmine

  • Includes assertions via expect
  • You can also choose an assertion library
  • Tried and tested
  • Support for BDD

Choosing is primarily a matter of preference

Chai

$npm install chai --save-dev

Chai Assertion Library

  • Verify something to be as expected
  • Provides regular assertions too
  • Provides chained language assertions
  • Compatible with Node.js and Browser
  • Available via Bower or NPM
'use strict';

var drinks = require('drinks'), // or window.drinks (depending on environment)
    chai = require('chai'), // or window.chai
    assert = chai.assert,
    expect = chai.expect;

describe('Drinks Module', function () {

    describe('#getCheapBeer', function () {
        it('Should return a cheap, nasty beer', function () {
            var beer = drinks.getCheapBeer();

            expect(beer).to.be.defined;
            expect(beer).to.be.a('string');
            expect(beer.toLowerCase()).to.equal('ducth gold');
        });
    });
});

Sample Code - Expect

Sample Code - Assert

'use strict';

var drinks = require('drinks'),
    chai = require('chai'),
    assert = chai.assert,
    expect = chai.expect;

describe('Drinks Module', function () {

    describe('#getCheapBeer', function () {
        it('Should return a cheap, nasty beer', function () {
            var beer = drinks.getCheapBeer();

            assert.notEqual(typeof beer, 'undefined');
            assert.equal(typeof beer ,'string');
            assert.equal(beer.toLowerCase(), 'ducth gold');
        });
    });
});

Testling

$npm install browserify testling

Get a Badge Showing Results

 "testling": {
    "harness": "mocha-bdd",
    "files": "test/*.js",
    "browsers": [
      "ie/8..latest",
      "chrome/22..latest",
      "firefox/16..latest",
      "safari/4..latest",
      "opera/11.0..latest",
      "iphone/6..latest",
      "ipad/6..latest",
      "android-browser/latest"
    ]
  }

Update package.json

​Add a webhook on github

Karma

Install

$npm install -g karma

Node Module Style Config


module.exports = function(config) {
  config.set({
    frameworks: ['mocha', 'chai'],

    files: [
      // Put Mocha/Jasmine tests here
      './test/moduleA.js'
    ],

    reporters: ['mocha'],
    port: 9876,
    colors: true,
    logLevel: 'INFO',
    captureTimeout: 60000,

    autoWatch: false,

    browsers: ['Chrome', 'Safari', 'Firefox'],

    singleRun: true
  });
};

Run

$karma start

Sample Karma Output

Mocha Client

An Example

Setup a test dir

mocha init path/to/testdir

Mocha Init

  • Creates an index.html
  • Includes mocha.js & mocha.css
  • Creates an empty test.js file

Demo

For testing let's use

  • Karma
  • Mocha
  • Angular Mocks

Angular Mocks?

Angular Mocks

  • Provides testing utilities
  • Allows loading of modules
  • Provides DI for testing functions
  • Mocks out core services e.g $http
  • Requires no changes to application logic

Testing a service

'use strict';

describe('Number Service', function () {
  var Numbers;

  // Create an app instance "module" comes from Angular mocks
  // 'myApp' is the name of our angular.module instance our code creates
  beforeEach(module('myApp'));

  // Get a new Service instance ,"inject" comes from Angular mocks
  beforeEach(inject(function (_Numbers_) {
    Numbers = _Numbers_;
  }));

  describe('#getInRange', function () {
    it('Should get number in range specified', function () {
      var n = Numbers.getInRange(10, 90);

      expect(n).to.be.a('number');
      expect(n).to.be.within(10, 90);
    });
  });
});

Remember!

 

Tests that use promises are asynchronous regardless of the code within.

 

Always.

Async (Mocha) Example

describe('Number Service', function () {
  var Numbers;

  beforeEach(module('myApp'));
  beforeEach(inject(function (_Numbers_) {
    Numbers = _Numbers_;
  }));

  describe('#getInRangeAsync', function () {
    it('Should get number in range specified', function (done) {
      Numbers.getInRangeAsync(10, 9).then(function (n) {
        expect(er).to.be.null;
        expect(n).to.be.a('number');
        expect(n).to.be.within(10, 90);
        done();
      }, done); // Done, when passed an arg, marks the test as failed
    });         // so we can pass done as the error callback even if we a
  });           // aren't expecting an error. 
});

Do we need a Server?

Client-Side Unit Tests

Absolutely not!

FeedHenry Mocks

  • No FeedHenry Node.js instance required
  • Mock out Act/Cloud requests in test cases
  • Set expected responses for calls
  • Unit test any application logic
  • Easier testing - Perform requests synchronously*
  • Available on NPM for download

* If using promises this may not work. You will still need to test asynchronously.

Example Auth Controller

App.Controllers.Auth = (function ($fh) {
    var session = null;

    this.login = function (username, password, callback) {
        $fh.act({
            act: 'login',
            req: { u: username, p: password }
        }, function (res) {
            if (res.session) {
                session = res.session;
                callback(null, true);
            } else {
                // Invalid user/pass etc...
                callback(null, false);
            }
        }, callback); // <--- Being lazy!
    };

    this.isLoggedIn = function () {
        return (session !== null);
    };
    
    return this;
})(window.$fh);;

Example Auth Test

describe('Auth', function () {
    var VALID = { u: 'bruce.w', p: 'iamthebatman' };

    describe('#login', function () {
        it('Should not return an error', function () {
            // The magic beings...
            $fh.act.expect({
                act: 'login',
                req: VALID
            }).setResponse(null, {
                session: 'SOME_RANDOM_SESSION_KEY'
            });

            // This will trigger the Act call we just set an expectation for!
            App.Controllers.Auth.login(VALID.u, VALID.p, function (err, loggedIn) {
                expect(err).to.be.null;
                expect(loggedIn).to.equal(true);
            });

            // Respond to any queued Act calls (like the one above via Auth)
            $fh.act.flush();
        });
    });
});

Example / CASE STUDY

♡'s

ng FeedHenry

  • Small project on feedhenry-staff GitHub
  • FeedHenry SDK Wrapped for Angular
  • Supports Dependency Injection
  • Allows use of Act and Cloud APIs
  • Uses Browserify for builds
  • Provides examples of testing Angular Code

Why Do we Need THis?

  • AngularJS has a lifecycle
  • $fh doesn't understand Angular's lifecycle
  • Variable updates can go unnoticed
  • Removes use of globals (well, kinda!)
  • Keeps code modular
  • Adds promises to $fh use
  • Provides neat error reporting (Act only for now!)

Act Example

angular.module('myApp', [
    'ng', 
    'ngFH' // <-- Awesomesauce
]);

angular.module('myApp').service('Auth', function ($scope, $q, Act) {
    // Other code...

    $scope.checkLogin = function (u, p) {
        // Act.request returns a promise
        return Act.request({
            act: 'login' ,
            req: {
                u: u,
                p: p
            }
        });
    };
});

angular.module('myApp').controller('LoginPage', function () {
    // Other code...

    $scope.doLogin = function () {
        Auth.checkLogin($scope.form.username, $scope.form.password)
            .then(successFn, failFn);
    };
});

DEMO TIME!

Totes Amazeballs!

Questions?

LINKS

JavaScript Unit Testing

By Evan Shortiss

JavaScript Unit Testing

An introduction to unit testing for JavaScript applications running on a client or server (Node.js) These slides cover, Mocha/Jasmine, Chai, Karma, AngularJS and FeedHenry testing practices.

  • 1,448