Framework: Gulp + Karma + Mochajavascript
Summary PPT: Pls refer to file tab 'JS-UT-With-Mocha'css
Path of JS files need test:html
Path .js unit test files using mocha should be in :java
And there are two utils common js files need in mocha test:node
Since that karma will import all JS source files at once when using it to run mocha test, we can't easily define the dependencies in each test file. And if use the original dev source dependencies defined, there is no better way to find the dependencies as well as the sub-dependencies which are used by the object need test. Also we want to define the dependencies used in the list so that all test files will not impact each other.jquery
There is a way to resolve this problem: create a helper method to get the object with the dependencies defined in the test file.web
modulizr.getCloneModulechrome
This method will be used in each test file to get the object need test with it's dependencies defined in test file, and the dependencies defined are used only by itself, not used by other test file.npm
/**
* This is used for JS test, to get the object with the dependencies
* @param name
* @param childVals
* @returns {*}
*/
modulizr.getCloneModule=function (name,childVals) {
var node=dependencyMap[name];
return node[FACTORY].apply(global, childVals);
};
// to get the object tested with the dependencies which are defined in test file
function require_ads() {
return modulizr.getCloneModule('ads',
[
require('jquery'),
require('uitk_ads')
]);
}
// the define for object need test
define('ads', [
'jquery', 'uitk'
], function ($, uitk) {
});
Need attention:json
modulizr.resetDependency
This method will be used to reset the dependency required in test cases.
In karma mocha test running, when require a same dependency in different test case, the two objects required are the same. If in the former case the dependency has been updated some values, it will impact the later case. So we need to reset the dependency before test which will use the dependency.
/**
* reset dependencies between tests so modules don't hold state
* @param deps
*/
modulizr.resetDependency = function(deps) {
if (!deps || deps.length === 0) {
throw new Error('RetDependency after test without dependency.');
}
if (typeof deps === 'string') { // handle the case where the caller only gives us a single string
deps = [deps];
}
for(var i=0; i<deps.length; i++){
var dep = dependencyMap[deps[i]];
if(dep) {delete dep.value;}
}
};
window.modulizr.resetDependency(
[
'modelData_offersResponseModel',
'infositeData_offersResponseModel'
]);
When to reset the dependency? If the dependency will be required in test case and it's value updates for that case, then you should reset the dep in order not to impact other cases. If only one test suite uses the dependency and update it's values, then you can call the reset method in beforeEach just for the test suite. Not need before all suite. Also not need reset all dependencies. And if only one case use the dependency and update the value, you can reset the dependency in the end of the case.
Refer to https://mochajs.org/. and https://mochajs.org/#interfaces.
In this project the test file is written in .js format. How to run it will be described in next section 'how to use karma'.
An Example
hotel-infosite-web\src\main\webapp.test\javascript.unit.tests\infosite.responsive\ads_mocha-tests.js
Need attention:
Use mocha.setup('qunit');
Use (function() { //the test script test }());
Define the dependencies with different name from others test files, such as 'uitk_ads', 'utils_airAttachBannerView' avoiding duplicate define issue
For the dependencies which are not in the dependencies list and not defined in dev source code but required in the dev source code, it's better to define it in a common test-utils.js file. Therefor it can be used by multiple files.
/**
* rules for JS unit test
* 1. use modulizr.getCloneModule to get object need test with the dependencies defined in test file itself or mocha-test-bundle.js
* 2. define the dependencies with different name from others test files, such as 'uitk_ads', 'utils_airAttachBannerView' avoiding duplicate define issue
* 3. for the dependencies which are not in the dependencies list and not defined in dev source code but required in the dev source code,
* it's better to define it in a common test-utils.js file. Therefor it can be used by multiple files.
* 4. for the dependencies which are not in the dependencies list, required in dev source code, and defined in dev source code,
* it can be required in test case, then stub it using sinon.js, remember to restore it.
* 5. use modulizr.resetDependency to reset the dependency required in test cases, to make the cases independent
* 6. use (function(){}()); to include the test script, so that no impact between each test file
* 7. use mocha.setup('qunit');
*/
(function() {
mocha.setup('qunit');
var windowExpads = window.expads;
// define('expads', function () {
// return {
// extensions: {
// clingyAds: {
// init: function () {
//
// }
// }
// }
// };
// });
define('uitk_ads', function () {
return {
subscribe: function () {
},
mediaquery: {
register: function () {
}
}
};
});
function require_ads () {
return modulizr.getCloneModule('ads',
[
require('jquery'),
require('uitk_ads')
]);
};
before(function () {
$(document.body).append('<div id="fixture-ads"></div>');
});
after(function () {
$("#fixture-ads").remove();
});
suite('ads');
test('Verify all ads dependencies are present', function () {
assert.ok( require_ads(), 'dependencies should be met');
});
suite('ads');
afterEach(function () {
window.expads = windowExpads;
});
test('Verify resize with expads undefined', function () {
window.expads = undefined;
var error = new TypeError();
error.message = "Cannot read property 'utils' of undefined";
var ads = require_ads();
ads.resize();
assert.notOk(window.expads,'with undefined expads,the function does nothing')
});
test('Verify resize with utils undefined', function () {
window.expads = {};
var error = new TypeError();
error.message = "Cannot read property 'resize' of undefined";
var ads = require_ads();
ads.resize();
assert.deepEqual(window.expads,{},'with utils undefined,the function does nothing')
});
test('Verify resize with typeof resize is not function', function () {
window.expads = {utils: {resize: ''}};
var error = new TypeError();
error.message = "window.expads.utils.resize is not a function";
var ads = require_ads();
ads.resize();
assert.deepEqual(window.expads,{utils: {resize: ''}},'with typeof resize is not function,the function does nothing')
});
test('Verify resize with happy path', function () {
window.expads = {
utils: {
resize: function () {
}
}
};
var resizeStub = sinon.stub(window.expads.utils, 'resize');
var ads = require_ads();
ads.resize();
assert.ok(resizeStub.withArgs('LARGEFOOTERGOOGLE').calledOnce, 'window.expads.utils.resize has been called')
});
suite('run');
afterEach( function () {
$('#fixture-ads').html('');
});
/**
* expads.extensions.clingyAds is defined
* right2 is defined
*/
test('Verify run with target.id=RIGHT2', function () {
$('#fixture-ads').html('<div id="RIGHT2"></div><div id="R2"></div>')
var expads = require('expads');
var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
var uitk = require('uitk_ads');
var subscribeStub = sinon.stub(uitk, 'subscribe');
var ads = require_ads();
ads.run();
assert.ok(initStub.withArgs('#RIGHT2', '#ads-column', "#dcol-adsense-container", "#site-footer-background").calledOnce, 'the function init has been called');
initStub.restore();
subscribeStub.restore();
});
/**
* expads.extensions.clingyAds is defined
* right2 is undefined
* r2 is defined
*/
test('Verify run with target.id=R2', function () {
$('#fixture-ads').html('<div id="R2"></div>')
var expads = require('expads');
var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
var uitk = require('uitk_ads');
var subscribeStub = sinon.stub(uitk, 'subscribe');
var ads = require_ads();
ads.run();
assert.ok(initStub.withArgs('#R2', '#ads-column', "#dcol-adsense-container", "#site-footer-background").calledOnce, 'the function init has been called');
initStub.restore();
subscribeStub.restore();
});
/**
* expads.extensions.clingyAds is defined
* right2 is undefined
* r2 is undefined
*/
test('Verify run with right2 undefined and r2 undefind', function () {
var fireBeaconPixelStub = sinon.stub();
var expads = require('expads');
var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
var uitk = require('uitk_ads');
var subscribeStub = sinon.stub(uitk, 'subscribe');
var ads = require_ads();
ads.run();
assert.equal(initStub.callCount, 0, 'expads.extensions.clingyAds.init() has been never called');
subscribeStub.restore();
initStub.restore();
});
}());
In order to not interrupt current JS unit test coverage report, there is a new config file named karma.conf.mocha.js.
To run below command in hotel-infosite-web project root folder, it will run the .js test files and open a Chrome browser with a "DEBUG" button.
node node_modules/karma/bin/karma start karma.conf.mocha.js --browsers=Chrome --single-run=false
We can run the command in IDE Terminal or the cmd dialog.
We can debug the unit test in Chrome browser, click "DEBUG" button in above image, and you can see below test result in "localhost:9876/debug.html".
Click the keyboard "F12", then you can set breakpoints to debug the test.
In the config, it define the 'testJs" task to start karma server and run mocha test written in karma config file karma.conf.mocha.js. Also the coverage settings are in this file.
Comments: this config will not interrupt current test coverage way working with Qunit.
Since that in mocha debug.html there is no div like 'qunit-fixture', so we can't easily to add dom element for test.
We can use below code to add a div before test, and use $("#fixture-ads").html() to test dom element.
Comments: Due to karma run all the test files at the same time and they all use one same DOM element html page, if using the same name 'mocha-fixture' it will impact between files which having the div. For example, if one test file run completes and remove the div 'mocha-fixture', the later test file now hasn't completed running cases and need use the div but unfortunately the div has been removed, then the cases fail.
So the div name should be different for each file which need test DOM element.
The rule to name it as "fixture-dependencyName". such as "fixture-ads" for test ads.js, "fixture-availability" for test availability.js.
before(function () {
$(document.body).append('<div id="fixture-ads"></div>');
});
after(function () {
$("#fixture-ads").remove();
});
Take air-attach-banner-view.js and book-button-view.js as an example. In airAttachBannerView it returns Marionette.ItemView.extend({behaviors: {Countdown: {}}, and in bookButtonView it returns Marionette.ItemView.extend({behaviors: { Modal2: {}}.
Before test them we need require('marionette') and define Marionette.Behaviors.behaviorsLookup. If we define it in each test file with its own behavior modal, when running it will override so that the former test file cases will fail. Now we define it together in testUtil/test-utils.js as below used by the two test files. And do not need define it in single test file.
/**
* to define Marionette.Behaviors.behaviorsLookup for multiple test files before test starting
*/
before(function () {
var Marionette = require('marionette');
Marionette.Behaviors.behaviorsLookup = function () {
return {
Countdown: Marionette.Behavior,
Modal2: Marionette.Behavior
};
};
});
Take test "Verify isHighContrastMode" in utils_mocha-tests.js as an example. in the test we want method "document.defaultView.getComputedStyle(objA, null).color" return the test data value we want, then we set document.defaultView.getComputedStyle with a new function as below
document.defaultView.getComputedStyle = function () {
return {
color: 'rgb(31,41,59)'
}
}
Once do the update for document.defaultView.getComputedStyle, the original functionality of the method document.defaultView.getComputedStyle will be destroyed. it not only effect js code, but also effect js library, e.g: jQuery. when call method .css(), it return undefined.
what we need to do is as following:
first record the original value of document.defaultView.getComputedStyle:
var orginalComputedStyledocument = document.defaultView.getComputedStyle;
then do the update for document.defaultView.getComputedStyle
after getting the test data value, remember reset the document.defaultView.getComputedStyle with the original value:
document.defaultView.getComputedStyle = orginalComputedStyledocument;
If the dev code update the system or library method, we also need to reset the method to the original one, for example, method configureMarionette in configurator .js
configureMarionette: function () {
Marionette.Behaviors.behaviorsLookup = function () {
return behaviors;
};
...}
The dev code have updated Marionette.Behaviors.behaviorsLookup. So in the test we should record and reset to the original one as below:
var behaviorsLookup = Marionette.Behaviors.behaviorsLookup;configurator.configureMarionette();Marionette.Behaviors.behaviorsLookup = behaviorsLookup;