GitHub LinkedIn RSS
Showing posts with label Testing. Show all posts
Showing posts with label Testing. Show all posts
Tuesday, October 28, 2014

AngularJS E2E Testing with Protractor


In continuation to AngularJS series, today we'll discuss e2e or end-to-end testing of AngularJS applications. If you've been following the blog for a while, you must have noticed my numerous stressing the importance of unit testing using Jasmine and Karma and automating JavaScript testing with Grunt.js. The only thing left behind was e2e testing, of which we would talk today using Protractor for AngularJS applications.

What is E2E Testing?


End-to-end testing is a methodology used to test, whether the flow of an application is performing as designed from start to finish. The purpose of carrying out end-to-end tests is to identify system dependencies and to ensure that the right information is passed between various system components and systems.

In contrast to unit testing, which verifies the correct behaviour of various components separately, end-to-end testing verifies the entire flow of the application. From front end development perspective, it will be checking, whether JavaScript logic is reflected in the UI components. Good thing about Protractor is that we can write our end-to-end specs using Jasmine, so no knowledge of additional framework is needed.

angular-seed


To demonstrate the methodology, I'll be using angular-seed project, actually the whole article will be based on this repository. This project is an application skeleton for a typical AngularJS web app. You can use it to quickly bootstrap your angular webapp projects and dev environment for these projects. Installing the application is no-brainer, just follow the instructions in the repository - they are quite detailed. The reason I've chosen the seed project, was it had already had preconfigured Jasmine unit tests and e2e protractor tests in place. What is left is to understand the code :)

Unit testing with Karma


End-to-end testing doesn't replace the good old unit testing. It merely completes it to provide a comprehensive testing tookit. Let's take a look at Karma configuration file, karma.conf.js:
module.exports = function(config){
  config.set({

    basePath : './',

    files : [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/components/**/*.js',
      'app/view*/**/*.js'
    ],

    autoWatch : true,

    frameworks: ['jasmine'],

    browsers : ['Chrome'],

    plugins : [
            'karma-chrome-launcher',
            'karma-firefox-launcher',
            'karma-jasmine',
            'karma-junit-reporter'
            ],

    junitReporter : {
      outputFile: 'test_out/unit.xml',
      suite: 'unit'
    }

  });
};
There are two interesting things about it. One is the integration with JUnit reporter, which reports test results in JUnit xml format. It than can be parsed programmatically and used for various DevOps purposes. For it however to work, you'll have to add the following line, indicating the usage of the reporter:
reporters: ['progress', 'junit']
The second thing is including angular-mocks.js file. It contains supporting functions for testing AngularJS application. Let's take a look at spec defined in version_test.js and see them in action.
'use strict';

describe('myApp.version module', function() {
  beforeEach(module('myApp.version'));

  describe('version service', function() {
    it('should return current version', inject(function(version) {
      expect(version).toEqual('0.1');
    }));
  });
});
We can see here the usage of functions module and inject. Both work in pair. The former registers a module configuration code by collecting the configuration information, which will be used when the injector is created by inject function. The latter wraps a function into an injectable function. The inject() creates new instance of $injector per test, which is then used for resolving references. You can read more about these functions in the respective documentation pages. You can also read a great article about Angular and Jamine here. The unit tests are run, as usual, using Karma command:
karma start karma.conf.js

End-to-end testing with Protractor


Protractor is a Node.js program built on top of WebDriverJS, which is Node.js runner, similar to node-jasmine, of which we've talked in Jasmine and Node.js article. Installing the driver is easy using the npm:
npm install -g selenium-webdriver
Then we need to set-up the selenium environment by running the following command:
npm run webdriver-manager
The interesting thing about this command is that it is run through the npm. The executed commands can be found in package.json file under scripts section.
"scripts": {
    "postinstall": "bower install",

    "prestart": "npm install",
    "start": "http-server -a localhost -p 8000 -c-1",

    "pretest": "npm install",
    "test": "karma start karma.conf.js",
    "test-single-run": "karma start karma.conf.js  --single-run",

    "preupdate-webdriver": "npm install",
    "update-webdriver": "webdriver-manager update",

    "preprotractor": "npm run update-webdriver",
    "protractor": "protractor e2e-tests/protractor.conf.js"
  }
So executing the webdriver-manager command, will actually execute webdriver-manager update. However since we have a pre prefix followed by the same name on another section, preupdate-webdriver, this script will be executed first - npm install. As you see configuring scripts through package file, allows us a lot of flexibility ensuring everything is run in the desired order.

Once everything is in place, let's start our e2e testing by typing the following command:
npm run protractor
Anddddd, it doesn't work - of course it won't :) So what is the problem:
....
protractor e2e-tests/protractor.conf.js

Starting selenium standalone server...
Selenium standalone server started at http://10.0.0.5:36333/wd/hub

/home/victor/git/angular-seed/node_modules/protractor/node_modules/selenium-
webdriver/lib/webdriver/promise.js:1640
      var result = fn();
                   ^
Error: Angular could not be found on the page http://localhost:8000/app/index.html :
retries looking for angular exceeded
From looking at the log we see that the webdriver is up and running on port 36333 and Protractor tries to fetch the page from port 8000. Is this the problem? As we can see Protractor runs according to configuration file e2e-tests/protractor.conf.js. Let's have a look at it:
exports.config = {
  allScriptsTimeout: 11000,

  specs: [
    '*.js'
  ],

  capabilities: {
    'browserName': 'chrome'
  },

  baseUrl: 'http://localhost:8000/app/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  }
};
Very similar to Karma config, isn't it? Run the specs written in Jasmine using Chrome on localhost:8000. But what is 8000? If we put here the port of our WebDriver, 36333, it won't help either, since WebDriver runs the Protractor tests and not the page itself. So the solution is pretty straight forward - configure web server on port 8000 to serve our app. Any server. Apache, Jetty or IIS God forbid, what ever is close to your heart. Rerunning the previous command will result some flickering on the page and console will report the passed tests. The tests are configured in scenarios.js file. I'll show you just one of them:
describe('view1', function() {

  beforeEach(function() {
    browser.get('index.html#/view1');
  });


  it('should render view1 when user navigates to /view1',
    function() {
    expect(element.all(by.css('[ng-view] p')).first()
      .getText()).toMatch(/partial for view 1/);
  });
})
Pay attention that instead of testing the internal logic of application, it rather tests the end result displayed to the user. That is, take the text of item retrieved by css rule, [ng-view] p, and test if it matches the string partial for view 1. That why it's called e2e testing.

Hope you found this article useful and would try to Protractor in your own projects. Next time we'll discuss Protractor usage with non AngularJS sites and also compare it to additional utility called CasperJS.
Sunday, August 31, 2014

Automate JavaScript Testing with Grunt.js


So far we've learned how to test your JavaScript code with Jasmine and running them against Node.js and browsers with Karma. We've also got familiar with modular design patterns in JavaScript. And yet, somehow it seems that we're still missing one last puzzle piece connecting all the others, it's called Grunt.js.

What is it?


According to it's site:
In one word: automation. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes. After you've configured it, a task runner can do most of that mundane work for you—and your team—with basically zero effort.
Zero or not, there is a bit of effort in making everything play together, but no worry - we'll figure it out. So what's our plan?
  • Write classes, which are both usable in Node.js, Require.js and global environment.
  • Write Jasmine specs to test our code in both Chrome and Firefox
  • Write Karma and Node.js runners
  • Write Grunt task to automate the testing

Writing universal JavaScript classes


In the end we'll type one command to test our code from every aspect. Feeling excited? Let's start! All the code can be found in GitHub, to where I copied some code from my project called Raceme.js, JavaScript clustering algorithms framework (some harmless PR :) First one is Vector class, which wraps the JavaScript array with minor functionality:
(function () {
    'use strict';

    var Vector = function Vector(v) {
        var vector = v;

        this.length = function length() {
            return vector.length;
        };

        this.toArray = function toArray() {
            return vector;
        };
    };

    if (typeof define === 'function' && define.amd) {
        // Publish as AMD module
        define(function() {return Vector;});
    } else if (typeof(module) !== 'undefined' && module.exports) {
        // Publish as node.js module
        module.exports = Vector;
    } else {
        // Publish as global (in browsers)
        var Raceme = window.Raceme = window.Raceme || {};
        Raceme.Common = Raceme.Common || {};
        Raceme.Common.Vector = Vector;
    }
}());
Notice the lower part of the code, where we define our class as AMD module using Require.js, CommonJS module for Node.js and global class for window environment. To spice things up, we'll add additional class, PlaneMapper, which will depend on our Vector class. It exposes one method, mapVector, mapping 2-dimensional coordinate point into vector. The problem with writing dependent universal classes is the loading process. As you remember, Require.js and Node.js use different loading methods - asynchronous versus synchronous. loadDependencies method unifies the approaches into one loading process. Pay attention to continuation of declaration logic in line 29; Once we have our PlaneMapper object defined, we finalize the declaration depending upon the method.
(function () {
    'use strict';

    var COMMONJS_TYPE = 2, GLOBAL_TYPE = 3;
    var loadDependencies = function loadDependencies(callback) {
        if (typeof define === 'function' && define.amd) {
            // define AMD module with dependencies
            define(['common/Vector'], callback); // cannot pass env type
        } else if (typeof(module) !== 'undefined' && module.exports) {
            // load CommonJS module
            callback(require('../common/Vector.js'), COMMONJS_TYPE);
        } else {
            // Publish as global (in browsers)
            callback(Raceme.Common.Vector, GLOBAL_TYPE);
        }
    };
    loadDependencies(function (Vector, env) {
        var PlaneMapper = function () {
            var mapVector = function mapVector(node) {
                return new Vector([node.x, node.y]);
            };

            return {
                mapVector: mapVector
            };
        };

        // finalize the declaration
        switch(env) {
            case COMMONJS_TYPE:
                module.exports = PlaneMapper();
                break;
            case GLOBAL_TYPE:
                var Raceme = window.Raceme = window.Raceme || {};
                Raceme.DataMappers = Raceme.DataMappers || {};
                Raceme.DataMappers.PlaneMapper = PlaneMapper();
                break;
            default:
                return PlaneMapper();
        }
    });
}());

Writing universal Jasmine specs


Code is written, time for testing. We'll create two Jasmine specs, each for one of the classes. As in before, we start with Vector class:
(function () {
    'use strict';
    describe('Mappers', function () {
        var loadDependencies = function loadDependencies(callback) {
            if (typeof define === 'function' && define.amd) {
                // load AMD module
                define(['common/Vector'], callback);
            } else if (typeof(module) !== 'undefined' && module.exports) {
                // load CommonJS module
                callback(require('../../src/common/Vector.js'));
            } else {
                // Publish as global (in browsers)
                callback(Raceme.Common.Vector);
            }
        };
        loadDependencies(function (Vector) {
            var vector;
            describe('Vector', function () {
                beforeEach(function() {
                    vector = new Vector([1, 2, 3]);
                });
                it('check length', function () {
                    expect(vector.length()).toEqual(3);
                });

                it('check toArray', function () {
                    expect(vector.toArray()).toEqual([1, 2, 3]);
                });
            });
        });
    });
})();
Nothing new here - we load the Vector class prior to declaring the spec using the same technique. Same with our mapper, besides loading two classes.
(function () {
    'use strict';
    describe('Mappers', function () {
        var loadDependencies = function loadDependencies(callback) {
            if (typeof define === 'function' && define.amd) {
                // load AMD module
                define(['common/Vector', 'dataMappers/PlaneMapper'], callback);
            } else if (typeof(module) !== 'undefined' && module.exports) {
                // load CommonJS module
                callback(require('../../src/common/Vector.js'), 
                    require('../../src/dataMappers/PlaneMapper.js'));
            } else {
                // Publish as global (in browsers)
                callback(Raceme.Common.Vector, Raceme.DataMappers.PlaneMapper);
            }
        };
        loadDependencies(function (Vector, PlaneMapper) {
            var vector;
            describe('PlaneMapper', function () {
                var mapper, node;
                beforeEach(function() {
                    mapper = PlaneMapper;
                    node = {
                        x: 5,
                        y: 10
                    };
                });
                it('check mapping', function () {
                    vector = mapper.mapVector(node);
                    expect(vector.toArray()).toEqual([5, 10]);
                });
            });
        });
    });
})();

Configuring Jasmine spec runners


Testing Node.js modules is easy - just run the jasmine-node command with path to the specs.
jasmine-node test/spec
Moving on to browser testing. We'll start with easier case using global declarations. First we create Karma configuration file, karma.conf.js. The main interest is in files and browsers sections, where we define our source and spec files in correct order and browsers we want to test.
...
files: [      
  'src/common/*.js',
  'src/dataMappers/*.js',
  'test/spec/*Spec.js'
],
...
browsers: ['Chrome', 'Firefox'],
...
Then invoking the tests using karma command.
karma start karma.conf.js
Lastly, let's test our Require.js modules. Since the modules will by loaded by Require.js instead of Karma, a new Karma configuration file is required - karma.conf.require.js. The first difference appears in frameworks section, where we tell Karma to use Require.js framework. This will require installing additional package called karma-requirejs.
...
frameworks: ['jasmine', 'requirejs'],
...
files: [
    {pattern: 'src/common/*.js', included: false},
    {pattern: 'src/dataMappers/*.js', included: false},
    {pattern: 'test/spec/*Spec.js', included: false},
    'test/test-require-main.js'
],
...
Additional difference comes in files section. Here we inform the test runner not to load our source and spec files. So why to list them at all? Listing the files enables us to use them later, during configuration of Require.js in test-require-main.js. Usually Require.js configuration appears in JavaScript file, mentioned in data-main attribute of script tag. However since we don't want to load HTML files, we configure our modules in test-require-main.js.
(function () {
    'use strict';
    var tests = [];
    for (var file in window.__karma__.files) {
        if (window.__karma__.files.hasOwnProperty(file)) {
            if (/Spec\.js$/.test(file)) {
                tests.push(file.replace(/^\/base\//,
                 'http://localhost:9876/base/'));
            }
        }
    }

    requirejs.config({
        // Karma serves files from '/base'
        baseUrl: 'http://localhost:9876/base/src/',

        // ask Require.js to load these files (all our tests)
        deps: tests,

        // start test run, once Require.js is done
        callback: window.__karma__.start
    });
}());
At first we pass through each file listed in the configuration by using window.__karma__.files list and initiate spec files list. While doing so, we adjust the domain of the specs modules to one used by Karma - localhost:9876. It will also be used as a baseUrl attribute in Require.js configuration. Then we integrate Require.js and Karma together by passing Karma's stating method, window.__karma__.start, as a callback in line 21. The heart of the fusing appears in line 18, where we configure to load our specs prior to calling the callback. Once specs are loaded, callback will be invoked starting the testing.

Writing Grunt tasks


As promised, it's time to integrate all parts using Grunt.js. For this to happen, we'll require four packages: grunt, grunt-cli and grunt-karma, grunt-jasmine-node. The first two for running the tasks and the rest are for calling Karma and Node.js runners. Make sure to install the packages locally into project's folder, otherwise it will not work. In fact all the packages should be installed locally, when you work with Grunt.js.

Installing them can be done easily using package.json and bower.json files. Once the files are in place just call appropriate install commands. It will download all the packages automatically into project's folder.
npm install
bower install
If you an eager environmentalist like me, who doesn't wish to store anything, but essential data on your repository, you may use .gitignore file, which tells Git to ignore specified paths.
node_modules/
bower_components/
Grunt tasks are defined using JavaScript code in gruntfile.js.
(function () {
    'use strict';
    module.exports = function(grunt) {
        grunt.initConfig({
            pkg: grunt.file.readJSON('package.json'),
            karma: {
                unit_global: {
                    configFile: 'karma.conf.js'
                },

                unit_requirejs: {
                    configFile: 'karma.conf.require.js'
                }
            },
            jasmine_node: {
                options: {
                    forceExit: true,
                    match: '.',
                    matchall: false,
                    extensions: 'js',
                    specNameMatcher: 'spec'
                },
                all: ['test/spec/']
            }
        });

        grunt.loadNpmTasks('grunt-karma');
        grunt.loadNpmTasks('grunt-jasmine-node');
        grunt.registerTask('default', ['jasmine_node', 
            'karma:unit_global', 'karma:unit_requirejs']);
    };
}());
Not very intimidating, isn't it? Basically what it does is configures our test tasks, loads the required packages and then runs the tasks. Now in details. At first it configures our Karma tasks by specifying two children in karma node: unit_global and unit_requirejs, each states it's configuration file name. Then it configures Node.js runner. Since it doesn't have any configuration file, all the settings are listed here. In the end, it runs the tasks in the order they appear in parameter array of registerTask method. Notice the usage of semicolon, when Karma tasks are specified. It tells Grunt to run specific tasks under karma node.

Tasks names can be changed, both jasmine_node and karma node's names cannot.


Aren't you eager to see the results?
grunt
Grunt will load and run the gruntfile.js file emitting the following result:
Running "jasmine_node:all" (jasmine_node) task
Common
    Vector
        check length
        check toArray
Mappers
    PlaneMapper
        check mapping
Finished in 0.014 seconds
3 tests, 3 assertions, 0 failures

Running "karma:unit_global" (karma) task
INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [launcher]: Starting browser Firefox
INFO [Chrome 36.0.1985]: Connected on socket HrOcIkaJ5aqQG85SOqIS
with id 63263274
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs)
INFO [Firefox 31.0.0]: Connected on socket wrrkgK5_skzDJztmOqIT wi
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.032 secs / 0.005 secs)
Firefox 31.0.0: Executed 3 of 3 SUCCESS (0.026 secs / 0.002 secs)
TOTAL: 6 SUCCESS

Running "karma:unit_requirejs" (karma) task
INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [launcher]: Starting browser Firefox
INFO [Chrome 36.0.1985]: Connected on socket PXxh9c5vacKQovhSOsI2
with id 36823086
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs)
INFO [Firefox 31.0.0]: Connected on socket Xu3qldD3wfmNskyOOsI3 wi
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs
Chrome 36.0.1985: Executed 3 of 3 SUCCESS (0.004 secs / 0.002 secs)
Firefox 31.0.0: Executed 3 of 3 SUCCESS (0.005 secs / 0.002 secs)
TOTAL: 6 SUCCESS

Done, without errors.
Perfection! But it's only a tip of the iceberg. We'll be talking more about Grunt.js using conditional logic and reporting, so stay tuned ;)
Sunday, July 20, 2014

Jasmine and Node.js


Following our last article about using Jasmine and Karma together, let's look how we streamline the testing of our server side code written in Node.js. To do this, we'll need to install jasmine-node package.
npm install -g jasmine-node
If you're not familiar with the tool, please read the JavaScript Developer Toolkit before proceeding any further. The installed version will only support Jasmine 1.3 version. In order to add the 2.0 support, you should install the branched version.
npm install -g jasmine-node@2.0.0-beta4
Again as in previous article we'll be using the specs from our previous Jasmine article and the source code can be found in GitHub. After installing the correct version, we'll have to make our Player and Song classes as CommonJS modules. If you don't know how, please read the article about Modular Design Patterns in JavaScript. And of course requiring them in our specs:
...
module.exports = Song;
...
module.exports = Player;
describe("Player", function() {
  var Player = require('../src/Player.js');
  var Song = require('../src/Song.js');
  ...
Everything is ready - let's run the spec. We do this by pointing the path of the spec
jasmine-node spec/
Andddd bummer - we receive an error:
Exception loading helper: c:\GitHub\jsdeepdive-jasmine-and-nodejs\spec\SpecHelper.js
[ReferenceError: beforeEach is not defined]
This happens because customer helpers are not supported :(
Removed Support for Custom Helpers (have to be inside a beforeEach, this is a jasmine change, check out their docs on how to write one)
So let's put our helper inside the spec.
  beforeEach(function() {
    player = new Player();
    song = new Song();

    jasmine.addMatchers({
        toBePlaying: function () {
          return {
            compare: function (actual, expected) {
              var player = actual;

              return {
                pass: player.currentlyPlayingSong ===
                  expected && player.isPlaying
              }
            }
          };
        }
      });
  });
Trying again starts to passing through specs, but fails on the last one:
Failures:
  1) Player #resume should throw an exception if song is already playing
    Message:
      Expected function to throw an Error, but it threw Error: song is already
      playing.
    Stacktrace:
      Error: Expected function to throw an Error, but it threw Error: song is
      already playing.
    at Object. (c:\GitHub\jsdeepdive-jasmine-and-nodejs\spec\PlayerSpec.js:71:10)
Finished in 0.007 seconds
5 Tests, 1 Failures, 0 Skipped
And this is strange, cause we for sure know that example specs should all pass. This happens because of the bug/feature of jasmine-node (don't forget it's still a Beta) and we'll change our code a bit to bypass the problem by using toThrow instead of toThrowError:
expect(function() {
  player.resume();
}).toThrowError("song is already playing");
expect(function() {
  player.resume();
}).toThrow(new Error("song is already playing"));
Hooray - it works now and all the spec pass. Yes it's a bit quirky and yes more work is needed, but it's a progress. Next time I'll show how to use Grunt, which offers a better support for Jasmine 2.0.
Sunday, July 13, 2014

Jasmine and Karma


Let's talk about testing again, and will surely again in the future :) Last time I introduced the JavaScript Testing with Jasmine and we ran a few specs, however it seemed some how half baked - you were required to create a special page and open it in the browser to see the results. I you wanted to check in various browsers, you should have iterated the same procedure on each browser again and again.

This is not how we do things in 2014! We want to streamline the process! This is where Karma comes. It is built on Node.js and allows you to run the tests of your front end code automatically on various browsers.

Installation


In order to install Karma on your computer, run the following npm command. If you're not familiar with the tool, please read the JavaScript Developer Toolkit article first:
npm install -g karma
After you try to call karma though, you'll get unknown command error, which can be easily solved by installing karma command-line interface (CLI):
npm install -g karma-cli
To initiate the framework, just run karma init and follow the steps.
Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next quest
ion.
> Chrome
>

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> spec/*.js
> src/*.js
>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes
Please notice the positive answer for the last question. If you running Karma stand alone, it should be set to true. On contrary it can be integrate with IDE and be executed from there, for instance WebStorm. Watch a very short tutorial about the integration of Karma with WebStorm. In the end of the initiation process, a configuration file karma.conf.js will be created, which will be used to run our tests. If you didn't specify the browsers or wanted to add another one, on which you'd like to run your tests, you can install them later from the list of supported browser launchers. To add Firefox support, run the following command to install it and add Firefox key to the browsers array in the configuration file:
npm install -g karma-firefox-launcher
Last touch is needed to be completely ready to go. Karma is a framework, which executes Jasmin's spec using karma-jasmine plugin. Once you specify it in the initiation process, the plugin is downloaded and installed in the global repository. However it installs 1.3 version and not the latest 2.0. To fix this, we'll download the correct version manually:
npm install -g karma-jasmine@2_0

Running the tests


We'll be using our test specs from our previous Jasmine article and the source code can be found in GitHub.
karma start karma.conf.js
INFO [karma]: Karma v0.12.21 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 36.0.1985 (Windows 8.1)]: Connected on socket DOFTiS5FuL20HriB6iUZ
with id 19929084
Chrome 36.0.1985 (Windows 8.1): Executed 5 of 5 SUCCESS (0.021 secs / 0.006 secs
Pardon my using Windows 8 - I had to give my Ubuntu laptop for repair :) Anyway, you'll notice your browser being stuck and closing it will just rerun all the tests. To fix this, set singleRun property in your configuration file to true. Also be aware of the localhost:9876 address. It is used by Karma to run the specs, which means you can test it on any device connected to your network. Try pointing your phone’s browser to Karma by looking at the URL of one of the browser windows running the tests. Because Karma is running an instance of Node.js, your test machine is acting like a server and will send the tests to any browser that is pointed to it.

We'll talk about using Jasmine with your server side using jasmine-node package in the next article.
Sunday, June 15, 2014

JavaScript Testing with Jasmine


For years, JavaScript developers checked their code by amm uhmm - exactly, they didn't. The QA, if there was one, tested the overall UI end to end. However no one really checked the code as it was accustomed with server side languages.

Later testing frameworks started to emerge. QUnit pioneered the domain, which was followed by Jasmine and in the end Mocha appeared. All these framework matured with time and became standard in the industry. Nowadays, there is absolutely no excuse for JavaScript developer to write a code without writing the tests.

Which one?


As I've mentioned, currently there are three frameworks, which are mature enough to be considered by us. I personally prefer Jasmine over others, since it's already packaged with test double function (spy) and assertion framework and offers fairly headless running. For those who prefer configuring different aspects and implementations of your testing framework should look at Mocha. Moreover have a look at a comparison article with pros and cons of all networks, and decide what suits you best. The syntax in all of them is nearly the same, so you can easily migrate from one to another.

Jasmine


Jasmine is a behavior-driven development framework for testing JavaScript code. Your tests are separated into suits, which contains several tests, called specs. Think of suit as of test case. To overview the framework, we will be using the examples provided with the Jasmine distribution, using the latest at the time of writing - 2.0.0. Look at the structure of our folders. Our classes reside under folder named src. There we have two files Player and Song, which has only one method throwing an exception.
function Player() {
}
Player.prototype.play = function(song) {
 this.currentlyPlayingSong = song;
 this.isPlaying = true;
};

Player.prototype.pause = function() {
 this.isPlaying = false;
};

Player.prototype.resume = function() {
 if (this.isPlaying) {
  throw new Error("song is already playing");
 }

 this.isPlaying = true;
};

Player.prototype.makeFavorite = function() {
 this.currentlyPlayingSong.persistFavoriteStatus(true);
};

function Song() {
}

Song.prototype.persistFavoriteStatus = function(value) {
  // something complicated
  throw new Error("not yet implemented");
};
We'll start by looking at a first suit:
describe("Player", function() {
  var player;
  var song;

  beforeEach(function() {
    player = new Player();
    song = new Song();
  });

  it("should be able to play a Song", function() {
    player.play(song);
    expect(player.currentlyPlayingSong).toEqual(song);

    //demonstrates use of custom matcher
    expect(player).toBePlaying(song);
  });
We see here a test suit, called Player, in which we declare one spec using it function. Notice the beforeEach function - everything inside it is run before each spec is executed. The real magic happens when expect function is executed. Firstly we call play method of Player class, which is supposed to assign the property currentlyPlayingSong with the passed parameter. After that we check if it indeed does what is supposed by executing expect(player.currentlyPlayingSong).toEqual(song). It performs exactly what it is written - expects the passed parameter to be equal to the song variable. If the variables are not the same, exception is thrown.

The next spec uses custom matcher declared in spec/SpecHelper.js, which performs the comparison based on several criterias.
beforeEach(function () {
  jasmine.addMatchers({
    toBePlaying: function () {
      return {
        compare: function (actual, expected) {
          var player = actual;

          return {
            pass: player.currentlyPlayingSong === 
              expected && player.isPlaying
          }
        }
      };
    }
  });
});
Jasmine comes with lots of build-in matchers, so before you create your own, make sure it isn't already defined. Moving on to the second suit and being amazed by more matchers :)
describe("when song has been paused", function() {
  beforeEach(function() {
    player.play(song);
    player.pause();
  });

  it("should indicate that song is currently paused", function() {
    expect(player.isPlaying).toBeFalsy();

    // demonstrates use of 'not' with a custom matcher
    expect(player).not.toBePlaying(song);
  });

  it("should be possible to resume", function() {
    player.resume();
    expect(player.isPlaying).toBeTruthy();
    expect(player.currentlyPlayingSong).toEqual(song);
  });
});  
Since this suit is nested within the first one, the beforeEach is "added" to the one declared earlier. Thus player and song variables will be already defined. ToBeTruthy and ToBeFalsy are a bit tricky at first, both refer to anything that is considered true and false in JavaScript like null, 0 and undefined. Play around with them in specially created Jasmine cheat sheet.
it("checks if current song has been made favorite", function() {
  spyOn(song, 'persistFavoriteStatus');

  player.play(song);
  player.makeFavorite();

  expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
});
In the beginning we tell Jasmine on which method we want to spy using spyOn function - persistFavoriteStatus method of instance song. Later we call some methods, which suppose to call the spied method and in the end we test if it was called and with which parameters.

We finish our presentation with the last suit, which demonstrates the ability to check for thrown exceptions using toThrowError function.
describe("#resume", function() {
  it("should throw an exception if song is already playing", 
    function() {
      player.play(song);

      expect(function() {
        player.resume();
      }).toThrowError("song is already playing");
    });
});
This is not exception handling feature and shouldn't be used in your live code. Use try-catch instead.

Don't worry if still feel you haven't grasped the topic yet, I'll be writing more about JavaScript testing and Jasmine in particular. Today was just an introduction and overview of the concept and main features.