JavaScript Unit Test: Steps toward more perfect code

by kyolee310

 

Why unit-test?

Javascript-based web applications—thanks to the tangled nature of their underlying event-driven system architecture—tend to spiral into the madness of the unregulated “event-handler” entropy explosion. In such cases, even the experts scratch their heads—wondering how seemingly innocent update in one component can trigger the creation of the Universe in another.

bigbang

Developers have tried to prevent such nightmares by writing detailed comments in the code—hoping to explain complex, interwoven relationships among the events in the applications. However, once the events become dependent upon one another across multiple modules, those supposedly helpful comments turn into lengthy tomes that even their authors cannot follow. Around that time, the developers start to ponder, “Don’t you think we should document this information separately?” Sure, but where?

If you are not fully aware of the problem’s extent, check out this example. Consider a simple event chain A—> B—> C. When an object gets updated in the application, it triggers Event A. Event A broadcasts a signal that awakens Event B. Then, Event B updates a variable that awakens Event C. The depth of the rabbit hole is anyone’s guess. Imagine that tens of the events are chained in this manner throughout the application. Consider yourself lucky if the chain happens to be linear. In some nightmare cases, the developers need to investigate the chain of interlinked events as though it’s a crime scene.

csi

To exacerbate the scenario, let’s add a new, naive developer who fails to recognize His Majesty (the chain A—> B—> C) in the application. Now, unknowingly the chain is broken, and this innocent, yet lethal mistake goes unnoticed. Later everyone shall learn that Event C is, in fact, only triggered when a certain condition is met. A month later, problems surface. Everyone is clueless. No one can explain when things went wrong. Everyone is devastated. His Majesty was never to be touched. It was clearly written in the comments. Have you not read the comments?

This is when unit-test comes in and restores order and sanity.

With unit-test, the original developer can prevent the event chain A—> B—> C from ever derailing by providing a set of tests that secures the event chain’s integrity. If any developer introduces changes in the code that disrupt the flow of those events, the unit-test will raise the red flag immediately and warn the developer about the unexpected consequence of the changes. With the automated test runners, such as Karma, the developers can instantly receive feedback while editing the code. Karma runs in the background on the developers’ machines, and it keeps an eye on the Javascript files. Whenever Karma detects any change in those Javascript files, it automatically triggers the unit-test in the background.

karmabanner

If comments and documents serve as a knowledge source, unit-test works as an enforcer that actively imposes a set of rules defined by the developers. Consider the unit-test as “live comments” working in action.

jasminejs

How to Write Jasmine Unit-test for Angular Module

Basic Jasmine Unit-test Setup

A simple Jasmine unit-test setup consists of describe() and it() blocks:

describe("<test category>", function() {
    it("<literal description of test>", function() {
        // test procedure comes here
        expect(<value>).MATCHER();
    });
});

The list of the Jasmine native matchers can be found in Jasmine 2.0 Documentation

Angular Mock and beforeEach()

See below Jasmine unit-test sample code for setting up Angular mock module in beforeEach(). The function beforeEach() will be called before each function it() is run within describe().

Jasmine Unit-test:

describe("SecurityGroupRules", function() {

    beforeEach(angular.mock.module('SecurityGroupRules'));
    var scope, httpBackend, ctrl;
    beforeEach(angular.mock.inject(function($rootScope, $httpBackend, $controller) {
        scope = $rootScope.$new();
        httpBackend = $httpBackend;
        ctrl = $controller('SecurityGroupRulesCtrl', {
            $scope: scope
        });
    }));

    describe("Initial Values Test", function() {
        it("Initial value of isRuleNotComplete is true", function() {
           expect(scope.isRuleNotComplete).toBeTruthy();
        });
    });
});

spyOn() and toHaveBeenCalled()

Notice the Angular function resetValues() below contains a function call cleanupSelections(). If you want to write a unit-test to ensure that the function cleanupSelections() gets executed whenever resetValues() is invoked, Jasmine’s spyOn() and toHaveBeenCalled() can be used as below:

Angular Module:

$scope.resetValues = function () {
    ...
    $scope.cleanupSelections();
    $scope.adjustIPProtocolOptions();
};

Jasmine Unit-test:

describe("Function resetValues() Test", function() {

    it("Should call cleanupSelections() after resetting values", function() {
        spyOn(scope, 'cleanupSelections');
        scope.resetValues();
        expect(scope.cleanupSelections).toHaveBeenCalled();
    });
});

setFixtures() – Template Loading

Not only ensuring function procedures, unit-test can also be used to prevent critical elements on a template from being altered.

The beforeEach() block below shows how to load a template before running unit-test. The template securitygroup_rules.pt will be loaded onto PhantomJS‘s environment so that a jQuery call, such as $('#inbound-rules-tab'), can be called to grab the static element on the template.

Jasmine Unit-test:

beforeEach(function() {
    var template = window.__html__['templates/panels/securitygroup_rules.pt'];
    // remove <script src> and <link> tags to avoid phantomJS error
    template = template.replace(/script src/g, "script ignore_src"); 
    template = template.replace(/\<link/g, "\<ignore_link"); 
    setFixtures(template);
});

describe("Template Label Test", function() {

    it("Should #inbound-rules-tab link be labeled 'Inbound'", function() {
        expect($('#inbound-rules-tab').text()).toEqual('Inbound');
    });
});

Notice above that template.replace() lines update the template’s elements to disable<script src=""></script> and <link></link>. When the template is loaded ontoPhantomJS, PhantomJS tries to continue loading other JS or CSS files appeared on the template. The loading of such files becomes an issue if their locations are not properly provided in the template — for instance, the files contain dynamic paths, then PhantomJS results in error since it will not be able to locate the files. A workaround for this issue is to disable <script> and <link> elements on the template and, instead, load such files directly using the karma configuration list karma.conf.js.

Template is required

In some cases, when a function contains calls that interact with elements on the template, then you will have to provide the template so that the function call can complete without error. For instance, the function cleanupSelections below contains jQuery calls in the middle of procedure. Without the template provided, the function will not be able to complete the execution since those jQuery lines will error out.

Angular Module:

$scope.cleanupSelections = function () {
    ...
    if( $('#ip-protocol-select').children('option').first().html() == '' ){
        $('#ip-protocol-select').children('option').first().remove();
    }
    ...
    // Section needs to be tested
    ...
};

setFixtures() – Direct HTML Fixtures

In some situations, the static elements provided by the template will be not satisfy the needed condition for testing the function. For instance, the functiongetInstanceVPCName below expects the select element vpc_network to be populated with options. In a real scenario, the options will be populated by AJAX calls on load — mocking such AJAX calls is described in the section below. However, if the intention is to limit the scope of testing for this specific function only, then you could directly provide the necessary HTML content in order to simulate the populated select options as seen in the setFixtures() call below:

Angular Module:

 $scope.getInstanceVPCName = function (vpcID) {
     ...
     var vpcOptions = $('#vpc_network').find('option');
     vpcOptions.each(function() {
         if (this.value == vpcID) {
             $scope.instanceVPCName = this.text;
         }  
     });
 }

Jasmine Unit-test:

beforeEach(function() {
    setFixtures('<select id="vpc_network">\
        <option value="vpc-12345678">VPC-01</option>\
        <option value="vpc-12345679">VPC-02</option>\
        </select>');
});

it("Should update instanceVPCName when getInstanceVPCName is called", function() {
    scope.getInstanceVPCName('vpc-12345678');
    expect(scope.instanceVPCName).toEqual('VPC-01');
});

Mock HTTP Backend

When writing unit-test for Angular modules, often it becomes necessary to simulate the interaction with the backend server. In that case, $httpBackend module can be used to set up the responses from the backend server for predetermined AJAX calls.

Angular Module:

    $scope.getAllSecurityGroups = function (vpc) {
        var csrf_token = $('#csrf_token').val();
        var data = "csrf_token=" + csrf_token + "&vpc_id=" + vpc;
        $http({
            method:'POST', url:$scope.securityGroupJsonEndpoint, data:data,
            headers: {'Content-Type': 'application/x-www-form-urlencoded'}
        }).success(function(oData) {
            var results = oData ? oData.results : [];
            $scope.securityGroupCollection = results;
        });
        ...
    }

Jasmine Unit-test:

describe("Function getAllSecurityGroups Test", function() {

    var vpc = 'vpc-12345678';

    beforeEach(function() {
        setFixtures('<input id="csrf_token" name="csrf_token" type="hidden" value="2a06f17d6872143ed806a695caa5e5701a127ade">');
        var jsonEndpoint  = "securitygroup_json";
        var data = 'csrf_token=2a06f17d6872143ed806a695caa5e5701a127ade&vpc_id=' + vpc 
        httpBackend.expect('POST', jsonEndpoint, data)
            .respond(200, {
                "success": true,
                "results": ["SSH", "HTTP", "HTTPS"]
            });
    });

    afterEach(function() {
        httpBackend.verifyNoOutstandingExpectation();
        httpBackend.verifyNoOutstandingRequest();
    });

    it("Should have securityGroupCollection[] initialized after getAllSecurityGroups() is successful", function() {
        scope.securityGroupJsonEndpoint = "securitygroup_json";
        scope.getAllSecurityGroups(vpc);
        httpBackend.flush();
        expect(scope.securityGroupCollection[0]).toEqual('SSH');
        expect(scope.securityGroupCollection[1]).toEqual('HTTP');
        expect(scope.securityGroupCollection[2]).toEqual('HTTPS');
    });
});

Also notice how setFixtures() is used in beforeEach() to prepare for the jQueryline var csrf_token = $('#csrf_token').val(); in the functiongetAllSecurityGroups().

Angular $watch test

$watch() is one of the most frequently used functions in Angular, which triggers events when it detects update in the watched object. When you need $watch() function to react in unit-test, you could call $apply() to have the latest update to be detected by the Angular module.

Angular Module:

$scope.setWatchers = function () {
    $scope.$watch('securityGroupVPC', function() {
        $scope.getAllSecurityGroups($scope.securityGroupVPC);
    });
};

Jasmine Unit-test:

it("Should call getAllSecurityGroupVPC when securityGroupVPC is updated", function() {
    spyOn(scope, 'getAllSecurityGroups');
    scope.setWatchers();
    scope.securityGroupVPC = "vpc-12345678";
    scope.$apply();
    expect(scope.getAllSecurityGroups).toHaveBeenCalledWith('vpc-12345678');
});

Angular $on test

In Angular,$on() is used to detect any broadcast signal from other Angular modules. For testing such setup, you could directly send out the signal by using $broadcast()call.

Angular Module:

$scope.setWatchers = function () {
    $scope.$on('updateVPC', function($event, vpc) {
        ...
        $scope.securityGroupVPC = vpc;
    });
};

Jasmine Unit-test:

it("Should update securityGroupVPC when updateVPC is called", function() {
    scope.setWatchers();
    scope.$broadcast('updateVPC', 'vpc-12345678');
    expect(scope.securityGroupVPC).toEqual('vpc-12345678');
});

Angular $broadcast test

Paired with $on(), you would also want to write unit-test for ensuring the$broadcast() call’s condition. For such purpose, spyOn() andtoHaveBeenCalledWith() setup can be used on $broadcast() to check for its proper signal signatures.

Angular Module:

$scope.setWatcher = function () {
    $scope.$watch('securityGroupVPC', function () {
        $scope.$broadcast('updateVPC', $scope.securityGroupVPC);
    });
};

Jasmine Unit-test:

it("Should broadcast updateVPC when securityGroupVPC is updated", function() {
    spyOn(scope, '$broadcast');
    scope.setWatcher();
    scope.securityGroupVPC = 'vpc-12345678';
    scope.$apply();
    expect(scope.$broadcast).toHaveBeenCalledWith('updateVPC', scope.securityGroupVPC);
});

For more detailed wiki on how to write Jasmine Unit Test for Angular Module, check out the link below:

https://github.com/eucalyptus/eucaconsole/wiki/JavaScript-UnitTest-Submit-Guideline

hphelion

euca_new_logo