JavaScript Testing

Madhan Ganesh

Why Test?

Because Writing  the test is easier than running the application

Principles

 

Predictable

 

 

 

Single Responsibility

 

 

Self Documenting

 

 

Tests Behaviour

 

 

Example to discuss

describe('add 2 numbers', function() {

    it('should return sum of both operands', function() {
    
        expect(calculator.add(1, 1)).to.be(2);

    });

});

Predictable

One thing at a time

Self documenting

Test behaviour

Excuses

It  is working, why to do more?

It will take too long and I need to know more!

My  code  just works fine

Unit  test  do not catch any bugs

Who cares? no business value

Oh!  takes time to run them

If writing unit  test is harder, listen carefully to your code... 

Tightly couples components

Singletons

Mixed Concerns

Asynchronous functions

Tools.. tools & tools

 

Karma

 

 

Jasmine

 

 

Sinon

 

 

Chai

 

 

Mocha

 

 

Protactor

 

 

Grunt

 

 

ngMock

 

Testing doubles with Jasmine spies

 

A test double is an object that acts and is used in place of another object.

Test double

 

Object under test

  • Count of calls on a function
  • The ability to specify a return value (stub a return value)
  • The ability to test arguments

Count of calls on a function

// Arrange
jasmine.spyOn(calculator, 'add');

// Act
calculator.add(2, 3);
calculator.add(3, 4);

// Assert
assert(calculator.add.count).is(2);
var calculator = {
    add: function(a, b) {
        var result = a + b;
        return result;
    }
};

Stub a return value


// Arrange
jasmine.spyOn(wordSource, 'getWord')
       .and
       .returnValue('simple');

// Act
var ret = wordManager.captilize();

// Assert
expect(ret).toBe('SIMPLE');
var wordManager = {
     
    capitalize: function(wordSource) {
        var word = wordSource.getWord(); // MY DEPENDENCY

        return word.toUpperCase(); // MY LOGIC
    }
}; 

Testing arguments


// Arrange
wordManager.setPrefix('Mr.');
jasmine.spyOn(wordSource, 'getWord');

// Act
var ret = wordManager.captilize();

// Assert
var callArgs = wordSource.getWord.call.argsFor(0);
expect(callArgs).toBe('Mr.');
var wordManager = {
    
    // .. logic to set 'myprefix'
 
    capitalize: function(wordSource) {
        var word = wordSource.getWord('myprefix  '); // MY DEPENDENCY

        return word.toUpperCase(); // MY LOGIC
    }
}; 

Asynchronous  Testing

  • setTimeout  &  setTimeInterval
  • UI effects
  • Ajax calls

Jasmine 2 ways

  • runs() & waitsFor()
  • Clock.useMock()

runs() & waitsFor()

var Calculator = 
    function(displayElement) {
        this.el = $(displayElement);
    };

Calculator.proptotype.hideResult = 
    function(callback) {
        this.$el
            .fadeOut(1000, callback);
    }
describe('Calculator', function() {

    var calc, el;
    
    beforEach(function() {
        el = $('<div>some content</div>');
        calc = new Calculator(el);
    });
    
    it('should hide the result', function() {
        // Arrange
        var flag = false;
        var  callback = function() {
            flag = true;
        });

        // Act
        runs(function() {
            calc.hideResult(callback);
        });
        
        waitsFor(function() {
            return flag;
        }, 'flag to be true', 1100);
    

        // Assert
        runs(function() {
            expect(el.css('display')).toBe('none');
        });    
    });
});

Clock.useMock()

function hideAfterSometime() {
    setTimeout(function() {
        hide  = true;
    }, 1000);
}
beforeEach(function() {   
    jasmine.clock().install();
});

afterEach(function() {
    jasmine.clock().uninstall();
});

it('should hide after 1 second', function() {
   
    // Act
    hideAfterSometime();
    jasmine.clock().tick(1001);

    // Assert    
    expect(hide).toBe(true);
});

Jasmine Spies

  • Built for mocking single functions
  • Inbuilt matchers
  • Supports:
    • Replacement mocking
    • Pass-through mocking
function readFile(fileName, onsuccess) {

    var f  = new FileReader(fileName);
    var data = f.readFile();

    onsuccess(data);

}

Spy Function

it('should invoke callback after file is read', function() {

    // Arrange
    var spyCallback = jasmine.createSpy('successcb');

    // Act
    readFile('some.txt', spyCallback);
    

    // Assert
    expect(spyCallback).toHaveBeenCalled();

});
function WorkitemsManager(worklistService) {
    this.exams = [];
    
    this.readExams = function() {
        this.exams = worklistService.getExams();
    }
}

Spy Method - mock return

it('should invoke callback after file is read', function() {

    // Arrange
    var exams = ['abc'];
    var worklistService = new WorklistService();
    spyOn(worklistService, 'getExams').andReturn(exams);
    

    // Act
    var workitemsManager = new WorkitemsManager(worklistService);
    workitemManager.readExams();
    

    // Assert
    expect(workitemsManager.exams).toEqual(exams);

});
function WorkitemsManager(worklistService) {
    this.exams = [];
    
    this.readExams = function() {
        this.exams = worklistService.getExams();
    }
}

Spy Method - and call through

it('should invoke callback after file is read', function() {

    // Arrange    
    var worklistService = new WorklistService();
    var spy  =  spyOn(worklistService, 'getExams').andCallThrough();
    

    // Act
    var workitemsManager = new WorkitemsManager(worklistService);
    workitemManager.readExams();
    

    // Assert
    expect(spy).toHaveBeenCalled();

});
function WorkitemsManager(worklistService) {
    this.exams = [];
    
    this.readExams = function() {
        try {
            this.exams = worklistService.getExams();
        } catch(ex) {
            this.exams = [];
        }        
    }
}

Spy Method - and throw

it('should invoke callback after file is read', function() {

    // Arrange    
    var worklistService = new WorklistService();
    spyOn(worklistService, 'getExams').andThrow(new Error('problem'));    

    // Act
    var workitemsManager = new WorkitemsManager(worklistService);
    workitemManager.readExams();    

    // Assert
    expect(workitemManager.exams).toBe([]);
});
module.directive('spinner', function (spinnerSvc) {
    return {
        link: function (scope, element) {
            scope.$on('showspinner', function () {
                spinnerSvc.spin(element[0]);
            });
            scope.$on('stopspinner', function () {
                spinnerSvc.stop(element[0]);
            });
        }
    };
});

Spy Object

 it('should show spinner on "showspinner" event', function () {
    
    // Arrange
    spinnerSvcMock = jasmine.createSpyObj('spinnerSvc', ['spin', 'stop']);

    // Act
    scope.$broadcast('showspinner');

    // Assert
    expect(spinnerSvcMock.spin).toHaveBeenCalledWith(element[0]);
});
function WorkitemsManager(worklistService) {
    this.exams = [];
    
    this.readExams = function() {
        worklistService.getExams(function(exams) {
            this.exams = exams;
        });
    }
}

Spy Method - mock callback

it('should invoke callback after file is read', function() {

    // Arrange
    var exams = ['abc'];
    var worklistService = new WorklistService();
    spyOn(worklistService, 'getExams').andCallFake(function(callback) {
        callback(exams);
    });
    

    // Act
    var workitemsManager = new WorkitemsManager(worklistService);
    workitemManager.readExams();
    

    // Assert
    expect(workitemsManager.exams).toEqual(exams);

});

Jasmine Spy Matchers

Jasmine Documentation

Testing Angular Objects

  • Controller
  • Service
  • Directive

Setup

describe('HeaderContoller', function () {

        
    beforeEach(module('app'));



    beforeEach(module(function($provide) {
        // inject mock services
    }));


    beforeEach(inject(function ($rootScope, $controller) {
        // create scope, controllers and mocks
    });



    it('should test something', function() {
        // Arrange

        // Act

        // Assert

    });

    ...
});

Start here

Load Module

Customize DI

Mock objects

Tests..

(function () {
    'use strict';

    describe('Header Controller', function () {
        var rootScope,
            controllerFactory,
            controller,
            scope;

        beforeEach(module('app'));

        beforeEach(inject(function ($rootScope, $controller) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
            controllerFactory = $controller;

            controller = controllerFactory('HeaderCtrl', {
                $scope: scope
            });
        }));

        it('it should ', function () {

        });

    });
})();

Testing Controller

(function() {
    describe('worklist service', function () {

        var worklistSvc;

        beforeEach(module('app'));

        beforeEach(inject(function (_worklistSvc_) {
            worklistSvc = _worklistSvc_;
        }));

        it('', function() {
        });
    });
})()

Testing Service

(function() {
    'use strict';

    describe('Datepicker  Directive', function() {

        var element, scope, compile;

        // setup - before each

        beforeEach(module('app'));

        beforeEach(inject(function($rootScope, $compile) {
            scope = $rootScope.$new();
            element = angular.element('{{!cursor}}');
            $compile(element)(scope);
            scope.$digest();

            compile = $compile;
        }));

        // Tests start here

    });
})();

Testing Directive

Benefits of Unit Test

  • Safety Net
  • Save Time
  • Makes it ease 
  • Makes development  fun!

Demo

Made with Slides.com