-
Notifications
You must be signed in to change notification settings - Fork 27
JavaScript UnitTest Submit Guideline
Install npm
, a package manager for JavaScript, if missing.
On CentOS
, run
yum install -y npm
At the project's home directory ./eucaconsole
, run
npm install
to install npm packages listed in the file package.json
.
The step above will install Grunt
, Karma
, and PhantomJS
dependencies to construct the JavaScript Unit-test environment on your machine. And, run
npm install -g grunt-cli
to allow grunt
command line tools to run on your console.
grunt karma
The command above will trigger Jasmine
unit-test and keep the test running in the background. When it detects that any of the JavaScript files listed in karma.conf.js
has been updated, it will re-trigger the unit-test automatically.**
- **On
vim
editor, you will need to set the option
:set backupcopy=yes
to allow the update of the JavaScript files to be detected. Without this setting, Karma
thinks the JavaScript file has been deleted on each file edit, which results in refusing to run the section of the unit-test related to the edited file.
grunt karma:ci
Unlike the grunt karma
command, it runs the unit-test only once and exits after. It is also known as Continuous Integration
mode used by Travis CI
.
The directory for Jasmine Spec files is located at
./eucaconsole/static/js/jasmine-spec
The Jasmine Spec files contains the implementation of the JavaScript unit-test where each Jasmine Spec file is mapped to the Angular module used in this project. For instance, the unit-test
spec_security_group_rules.js
maps to the Angular module,
security_group_rules.js
When testing the Angular module SecurityGroupRules
below, be sure that all the dependencies modules, ('SecurityGroupRules', ['CustomFilters', 'EucaConsoleUtils'])
are listed in karma.conf.js
file. If the test requires a template to be loaded, then the template also needs to be included in the list.
Angular Module:
angular.module('SecurityGroupRules', ['CustomFilters', 'EucaConsoleUtils'])
.controller('SecurityGroupRulesCtrl', function ($scope, $http, $timeout, eucaUnescapeJson) {
...
$scope.isRuleNotComplete = true;
...
});
karma.conf.js:
files: [
...
'templates/panels/*.pt',
'static/js/pages/custom_filters.js',
'static/js/pages/eucaconsole_utils.js',
'static/js/widgets/securitygroup_rules.js',
'static/js/jasmine-spec/spec_security_group_rules.js',
...
]
Angular missing module error
When seeing a lengthy, jumbled error message as below, it is likely the issue with missing Angular modules on karma.conf.js
:
Error: [$injector:modulerr] http://errors.angularjs.org/1.2.26/$injector/modulerr?p0=BucketContentsPage&p1=Error%3A%20%5B%24injector%3Amodulerr%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.2.26%
...
at /root/seeds/eucaconsole/eucaconsole/static/js/thirdparty/angular/angular.min.js:34
at r (/root/seeds/eucaconsole/eucaconsole/static/js/thirdparty/angular/angular.min.js:7)
...
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
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();
});
});
});
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();
});
});
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 onto PhantomJS
, 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
...
};
In some situations, the static elements provided by the template will be not satisfy the needed condition for testing the function. For instance, the function getInstanceVPCName
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');
});
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 jQuery
line var csrf_token = $('#csrf_token').val();
in the function getAllSecurityGroups()
.
$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');
});
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');
});
Paired with $on()
, you would also want to write unit-test for ensuring the $broadcast()
call's condition. For such purpose, spyOn()
and toHaveBeenCalledWith()
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);
});
Similar to $broadcast
, you can also test $emit
with the spyOn
approach above:
Angular Module:
$scope.$emit('tagUpdate');
Jasmine Unit-test:
spyOn(scope, '$emit');
...
expect(scope.$emit).toHaveBeenCalledWith('tagUpdate');
If Angular module contains instructions wrapped in $timeout()
scope, $timeout.flush()
call is needed in unit-test to ensure those instructions within $timeout()
have been completed.
The example below shows a sample unit-test that is to verify whether the function updateTerminationPoliciesOrder
gets called when $watch
event is triggered. Notice the target function updateTerminationPoliciesOrder
resides within $timeout()
scope. This means that, on Jasmine's unit-test, timeout.flush()
is needed to be called prior to examine the status of the target function updateTerminationPoliciesOrder
. Also, notice how $timeout
is injected into the Angular mock module, which is then passed to the unit-test spec function as a plain variable timeout
.
Angular Module:
$scope.setWatch = function () {
$scope.$watch('terminationPoliciesUpdate', function () {
$timeout(function (){
$scope.updateTerminationPoliciesOrder();
});
}, true);
};
Jasmine Unit-test:
var scope, ctrl, timeout;
beforeEach(angular.mock.inject(function($controller, $rootScope, $timeout) {
scope = $rootScope.$new();
// Handle $timeout() events in Angular module
timeout = $timeout;
ctrl = $controller('ScalingGroupPageCtrl', {
$scope: scope
});
}));
...
it("Should call updateTerminationPoliciesOrder when terminationPoliciesUpdate is updated", function() {
spyOn(scope, 'updateTerminationPoliciesOrder');
scope.setWatch();
scope.terminationPolicies = ['NewestInstance', 'ClosestToNextInstanceHour'];
scope.$apply();
timeout.flush();
expect(scope.updateTerminationPoliciesOrder).toHaveBeenCalled();
});