In this post I am setting up a NodeJS project, it has a test project to run automation tests, i also takes care of code coverage as well. The project uses following frameworks:
- NodeJS (https://github.com/nodejs)
- Istanbul middleware (https://github.com/gotwarlost/istanbul-middleware)
- ExpressJS (https://expressjs.com/)
- Protractor (https://github.com/angular/protractor)
In first step I am creating a NodeJS project:
- Install NodeJs
- Go to project folder for example C:\\
\ - Open command prompt/terminal in the above location, run the following commands:
npm init
npm install express
- After the above commands running successfully, create a file named server.js, and have the following code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express') | |
const app = express() | |
app.use(express.static(__dirname + '/public')); | |
app.listen(3001, function () { | |
console.log('Example app listening on port 3001!') | |
}) |
- In the same folder create a sub-folder named public, so in the above project we are creating a static server to serve static files like html and images. In the public folder create a HTML file named index.html. Following are the contents of index.html:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Test page</title> | |
<script src="public-coverage/src/module1/module_1_2/test.js"></script> | |
</head> | |
<body> | |
<h1>hello my name is asutosh , the number is <bold></bold></h1> | |
</body> | |
</html> |
- In the above public folder you may create a folder named src, under src folder you may have multiple sub-folders, each sub-folder may act as module, and each module may have single or multiple javascript files.
- Before running any testcase to test the above project all the code in src folder need to be instrumented.
- For instrumentation istanbul-instrumentor cli utility can be used. Keep the instrumented code in public-coverage folder.
- If you notice the above index.html one of the instrumented Javascript file is used, so when any automation test case launch this html file the coverage for the particular file will be collected, however other instrumented Javascript file's coverage won't be collected. However those non-loaded file's coverage should be actually 0%.
Let's create the test project which will run autonomous tests on the above project, basically it will load the index.html in browser, it will collect the coverage data and pushes it to istanbul coverage collector.
- Here Protractor is used to run autonomous tests, following is my Protractor configuration file: This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
exports.config = { framework: 'jasmine', seleniumAddress: 'http://localhost:4444/wd/hub', specs: ['spec.js'] } - Below is the content of spec.js file:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var request = require( "superagent" ); | |
describe('Protractor Demo App', function() { | |
it('should have a title', function() { | |
browser.waitForAngularEnabled(false); | |
browser.get('http://localhost:3001/test.html'); | |
browser.executeScript( | |
function(arg1) { | |
return window.__coverage__; | |
}, null).then(function(response){ | |
request | |
.post( "http://localhost:3002/coverage/client" ) | |
.set("Content-Type", "application/json") | |
.send(JSON.stringify(response)) | |
.end( function(resp,error){ | |
} ); | |
}); | |
expect(browser.getTitle()).toEqual('Test page'); | |
}); | |
}); |
- Now create the middle-ware NodeJS project which will run the Istanbul middle-ware(https://github.com/gotwarlost/istanbul-middleware) to handle get/post request for all coverage related data.
- Following is the middle-ware server code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var coverage = require('istanbul-middleware'); | |
var express = require('express'); | |
var path=require('path'); | |
var app = express(); | |
app.use(express.static(path.join("C:\\myspace\\nodetest\\public", 'public-coverage'))); | |
app.use('/coverage', coverage.createHandler({staticdir:"C:\\myspace\\nodetest\\public\\public-coverage"})); | |
app.listen(3002); |
- In the above code for staticdir's value you have to give the path where your instrumented code resides.
- In node_modules/archiver/lib/core.js you have to modify the code to do coverage calculation for non-loaded files during tests. Following is the file's modified code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* node-archiver | |
* | |
* Copyright (c) 2012-2014 Chris Talkington, contributors. | |
* Licensed under the MIT license. | |
* https://github.com/archiverjs/node-archiver/blob/master/LICENSE-MIT | |
*/ | |
var fs = require('fs'); | |
var inherits = require('util').inherits; | |
var Transform = require('readable-stream').Transform; | |
var async = require('async'); | |
var util = require('./util'); | |
var Archiver = module.exports = function(options) { | |
if (!(this instanceof Archiver)) { | |
return new Archiver(options); | |
} | |
options = this.options = util.defaults(options, { | |
highWaterMark: 1024 * 1024, | |
statConcurrency: 4 | |
}); | |
Transform.call(this, options); | |
this._entries = []; | |
this._format = false; | |
this._module = false; | |
this._pending = 0; | |
this._pointer = 0; | |
this._queue = async.queue(this._onQueueTask.bind(this), 1); | |
this._queue.drain = this._onQueueDrain.bind(this); | |
this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency); | |
this._state = { | |
aborted: false, | |
finalize: false, | |
finalizing: false, | |
finalized: false, | |
modulePiped: false | |
}; | |
}; | |
inherits(Archiver, Transform); | |
Archiver.prototype._abort = function() { | |
this._state.aborted = true; | |
this._queue.kill(); | |
this._statQueue.kill(); | |
if (this._queue.idle()) { | |
this._shutdown(); | |
} | |
}; | |
Archiver.prototype._append = function(filepath, data) { | |
data = data || {}; | |
var task = { | |
source: null, | |
filepath: filepath | |
}; | |
if (!data.name) { | |
data.name = filepath; | |
} | |
data.sourcePath = filepath; | |
task.data = data; | |
if (data.stats && data.stats instanceof fs.Stats) { | |
task = this._updateQueueTaskWithStats(task, data.stats); | |
this._queue.push(task); | |
} else { | |
this._statQueue.push(task); | |
} | |
}; | |
Archiver.prototype._finalize = function() { | |
if (this._state.finalizing || this._state.finalized || this._state.aborted) { | |
return; | |
} | |
this._state.finalizing = true; | |
this._moduleFinalize(); | |
this._state.finalizing = false; | |
this._state.finalized = true; | |
}; | |
Archiver.prototype._maybeFinalize = function() { | |
if (this._state.finalizing || this._state.finalized || this._state.aborted) { | |
return false; | |
} | |
if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { | |
this._finalize(); | |
return true; | |
} | |
return false; | |
}; | |
Archiver.prototype._moduleAppend = function(source, data, callback) { | |
if (this._state.aborted) { | |
callback(); | |
return; | |
} | |
this._module.append(source, data, function(err) { | |
this._task = null; | |
if (this._state.aborted) { | |
this._shutdown(); | |
return; | |
} | |
if (err) { | |
this.emit('error', err); | |
setImmediate(callback); | |
return; | |
} | |
this.emit('entry', data); | |
this._entries.push(data); | |
setImmediate(callback); | |
}.bind(this)); | |
}; | |
Archiver.prototype._moduleFinalize = function() { | |
if (typeof this._module.finalize === 'function') { | |
this._module.finalize(); | |
} else if (typeof this._module.end === 'function') { | |
this._module.end(); | |
} else { | |
this.emit('error', new Error('module: no suitable finalize/end method found')); | |
return; | |
} | |
}; | |
Archiver.prototype._modulePipe = function() { | |
this._module.on('error', this._onModuleError.bind(this)); | |
this._module.pipe(this); | |
this._state.modulePiped = true; | |
}; | |
Archiver.prototype._moduleSupports = function(key) { | |
if (!this._module.supports || !this._module.supports[key]) { | |
return false; | |
} | |
return this._module.supports[key]; | |
}; | |
Archiver.prototype._moduleUnpipe = function() { | |
this._module.unpipe(this); | |
this._state.modulePiped = false; | |
}; | |
Archiver.prototype._normalizeEntryData = function(data, stats) { | |
data = util.defaults(data, { | |
type: 'file', | |
name: null, | |
date: null, | |
mode: null, | |
sourcePath: null, | |
stats: false | |
}); | |
if (stats && data.stats === false) { | |
data.stats = stats; | |
} | |
var isDir = data.type === 'directory'; | |
if (data.name) { | |
data.name = util.sanitizePath(data.name); | |
if (data.name.slice(-1) === '/') { | |
isDir = true; | |
data.type = 'directory'; | |
} else if (isDir) { | |
data.name += '/'; | |
} | |
} | |
if (typeof data.mode === 'number') { | |
data.mode &= 0777; | |
} else if (data.stats && data.mode === null) { | |
data.mode = data.stats.mode & 0777; | |
} else if (data.mode === null) { | |
data.mode = isDir ? 0755 : 0644; | |
} | |
if (data.stats && data.date === null) { | |
data.date = data.stats.mtime; | |
} else { | |
data.date = util.dateify(data.date); | |
} | |
return data; | |
}; | |
Archiver.prototype._onModuleError = function(err) { | |
this.emit('error', err); | |
}; | |
Archiver.prototype._onQueueDrain = function() { | |
if (this._state.finalizing || this._state.finalized || this._state.aborted) { | |
return; | |
} | |
if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { | |
this._finalize(); | |
return; | |
} | |
}; | |
Archiver.prototype._onQueueTask = function(task, callback) { | |
if (this._state.finalizing || this._state.finalized || this._state.aborted) { | |
callback(); | |
return; | |
} | |
this._task = task; | |
this._moduleAppend(task.source, task.data, callback); | |
}; | |
Archiver.prototype._onStatQueueTask = function(task, callback) { | |
if (this._state.finalizing || this._state.finalized || this._state.aborted) { | |
callback(); | |
return; | |
} | |
fs.stat(task.filepath, function(err, stats) { | |
if (this._state.aborted) { | |
setImmediate(callback); | |
return; | |
} | |
if (err) { | |
this.emit('error', err); | |
setImmediate(callback); | |
return; | |
} | |
task = this._updateQueueTaskWithStats(task, stats); | |
if (task.source !== null) { | |
this._queue.push(task); | |
setImmediate(callback); | |
} else { | |
this.emit('error', new Error('unsupported entry: ' + task.filepath)); | |
setImmediate(callback); | |
return; | |
} | |
}.bind(this)); | |
}; | |
Archiver.prototype._shutdown = function() { | |
this._moduleUnpipe(); | |
this.end(); | |
}; | |
Archiver.prototype._transform = function(chunk, encoding, callback) { | |
if (chunk) { | |
this._pointer += chunk.length; | |
} | |
callback(null, chunk); | |
}; | |
Archiver.prototype._updateQueueTaskWithStats = function(task, stats) { | |
if (stats.isFile()) { | |
task.data.type = 'file'; | |
task.data.sourceType = 'stream'; | |
task.source = util.lazyReadStream(task.filepath); | |
} else if (stats.isDirectory() && this._moduleSupports('directory')) { | |
task.data.name = util.trailingSlashIt(task.data.name); | |
task.data.type = 'directory'; | |
task.data.sourcePath = util.trailingSlashIt(task.filepath); | |
task.data.sourceType = 'buffer'; | |
task.source = new Buffer(0); | |
} else { | |
return task; | |
} | |
task.data = this._normalizeEntryData(task.data, stats); | |
return task; | |
}; | |
Archiver.prototype.abort = function() { | |
if (this._state.aborted || this._state.finalized) { | |
return this; | |
} | |
this._abort(); | |
return this; | |
}; | |
Archiver.prototype.append = function(source, data) { | |
if (this._state.finalize || this._state.aborted) { | |
this.emit('error', new Error('append: queue closed')); | |
return this; | |
} | |
data = this._normalizeEntryData(data); | |
if (typeof data.name !== 'string' || data.name.length === 0) { | |
this.emit('error', new Error('append: entry name must be a non-empty string value')); | |
return this; | |
} | |
if (data.type === 'directory' && !this._moduleSupports('directory')) { | |
this.emit('error', new Error('append: entries of "directory" type not currently supported by this module')); | |
return this; | |
} | |
source = util.normalizeInputSource(source); | |
if (Buffer.isBuffer(source)) { | |
data.sourceType = 'buffer'; | |
} else if (util.isStream(source)) { | |
data.sourceType = 'stream'; | |
} else { | |
this.emit('error', new Error('append: input source must be valid Stream or Buffer instance')); | |
return this; | |
} | |
this._queue.push({ | |
data: data, | |
source: source | |
}); | |
return this; | |
}; | |
Archiver.prototype.bulk = function(mappings) { | |
if (this._state.finalize || this._state.aborted) { | |
this.emit('error', new Error('bulk: queue closed')); | |
return this; | |
} | |
if (!Array.isArray(mappings)) { | |
mappings = [mappings]; | |
} | |
var self = this; | |
var files = util.file.normalizeFilesArray(mappings); | |
files.forEach(function(file){ | |
var isExpandedPair = file.orig.expand || false; | |
var fileData = file.data || {}; | |
file.src.forEach(function(filepath) { | |
var data = util._.extend({}, fileData); | |
data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath); | |
if (data.name === '.') { | |
return; | |
} | |
self._append(filepath, data); | |
}); | |
}); | |
return this; | |
}; | |
Archiver.prototype.directory = function(dirpath, destpath, data) { | |
if (this._state.finalize || this._state.aborted) { | |
this.emit('error', new Error('directory: queue closed')); | |
return this; | |
} | |
if (typeof dirpath !== 'string' || dirpath.length === 0) { | |
this.emit('error', new Error('directory: dirpath must be a non-empty string value')); | |
return this; | |
} | |
this._pending++; | |
if (destpath === false) { | |
destpath = ''; | |
} else if (typeof destpath !== 'string'){ | |
destpath = dirpath; | |
} | |
if (typeof data !== 'object') { | |
data = {}; | |
} | |
var self = this; | |
util.walkdir(dirpath, function(err, results) { | |
if (err) { | |
self.emit('error', err); | |
} else { | |
results.forEach(function(file) { | |
var entryData = util._.extend({}, data); | |
entryData.name = util.sanitizePath(destpath, file.relative); | |
entryData.stats = file.stats; | |
self._append(file.path, entryData); | |
}); | |
} | |
self._pending--; | |
self._maybeFinalize(); | |
}); | |
return this; | |
}; | |
Archiver.prototype.file = function(filepath, data) { | |
if (this._state.finalize || this._state.aborted) { | |
this.emit('error', new Error('file: queue closed')); | |
return this; | |
} | |
if (typeof filepath !== 'string' || filepath.length === 0) { | |
this.emit('error', new Error('file: filepath must be a non-empty string value')); | |
return this; | |
} | |
this._append(filepath, data); | |
return this; | |
}; | |
Archiver.prototype.finalize = function() { | |
if (this._state.aborted) { | |
this.emit('error', new Error('finalize: archive was aborted')); | |
return this; | |
} | |
if (this._state.finalize) { | |
this.emit('error', new Error('finalize: archive already finalizing')); | |
return this; | |
} | |
this._state.finalize = true; | |
if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) { | |
this._finalize(); | |
} | |
return this; | |
}; | |
Archiver.prototype.setFormat = function(format) { | |
if (this._format) { | |
this.emit('error', new Error('format: archive format already set')); | |
return this; | |
} | |
this._format = format; | |
return this; | |
}; | |
Archiver.prototype.setModule = function(module) { | |
if (this._state.aborted) { | |
this.emit('error', new Error('module: archive was aborted')); | |
return this; | |
} | |
if (this._state.module) { | |
this.emit('error', new Error('module: module already set')); | |
return this; | |
} | |
this._module = module; | |
this._modulePipe(); | |
return this; | |
}; | |
Archiver.prototype.pointer = function() { | |
return this._pointer; | |
}; |
- As we have modified the core .js file the node_modules/istanbul-middleware/lib/handlers.js code has to be modified. Following is the code for handler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright (c) 2013, Yahoo! Inc. All rights reserved. | |
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. | |
*/ | |
var path = require('path'), | |
fs = require('fs'), | |
core = require('./core'), | |
istanbul = require('istanbul'), | |
bodyParser = require('body-parser'), | |
ASSETS_DIR = istanbul.assetsDir, | |
existsSync = fs.existsSync || path.existsSync, | |
url = require('url'), | |
archiver = require('archiver'), | |
ZipWriter = require('./zip-writer'), | |
express = require('express'), | |
Report = istanbul.Report, | |
Collector = istanbul.Collector, | |
utils = istanbul.utils, | |
JS_RE = /\.js$/; | |
/** | |
* Set default max limit to 100mb for incoming JSON and urlencoded | |
* @type {String} | |
*/ | |
var fileSizeMaximum = '100mb'; | |
var isExtended = true; | |
function createHandler(opts) { | |
/*jslint nomen: true */ | |
opts = opts || {}; | |
var app = express(); | |
// using separete options objects to maintain readability as the objects are getting more complex | |
var dirForAllFiles=opts.staticdir; | |
var urlOptions = { extended: isExtended, limit: fileSizeMaximum }; | |
var jsonOptions = { limit: fileSizeMaximum }; | |
//send static file for /asset/asset-name | |
app.use('/asset', express.static(ASSETS_DIR)); | |
app.use('/asset', express.static(path.join(ASSETS_DIR, 'vendor'))); | |
app.use(bodyParser.urlencoded(urlOptions)); | |
app.use(bodyParser.json(jsonOptions)); | |
//show main page for coverage report for / | |
app.get('/', function (req, res) { | |
var origUrl = url.parse(req.originalUrl).pathname, | |
origLength = origUrl.length; | |
if (origUrl.charAt(origLength - 1) !== '/') { | |
origUrl += '/'; | |
} | |
core.render(null, res, origUrl); | |
}); | |
//show page for specific file/ dir for /show?file=/path/to/file | |
app.get('/show', function (req, res) { | |
var origUrl = url.parse(req.originalUrl).pathname, | |
u = url.parse(req.url).pathname, | |
pos = origUrl.indexOf(u), | |
file = req.query.p; | |
if (pos >= 0) { | |
origUrl = origUrl.substring(0, pos); | |
} | |
if (!file) { | |
res.setHeader('Content-type', 'text/plain'); | |
return res.end('[p] parameter must be specified'); | |
} | |
core.render(file, res, origUrl); | |
}); | |
//reset coverage to baseline on POST /reset | |
app.post('/reset', function (req, res) { | |
core.restoreBaseline(); | |
res.json({ ok: true }); | |
}); | |
//opt-in to allow resets on GET as well (useful for easy browser-based demos :) | |
if (opts.resetOnGet) { | |
app.get('/reset', function (req, res) { | |
core.restoreBaseline(); | |
res.json({ ok: true }); | |
}); | |
} | |
//return global coverage object on /object as JSON | |
app.get('/object', function (req, res) { | |
res.json(core.getCoverageObject() || {}); | |
}); | |
//send self-contained download package with coverage and reports on /download | |
app.get('/download', function (req, res) { | |
var stream = archiver.createZip(), | |
writer = new ZipWriter(stream, process.cwd()), | |
coverageObject = core.getCoverageObject() || {}, | |
collector = new Collector(), | |
baseDir = process.cwd(), | |
reports = [ | |
Report.create('html', { writer: writer, dir: path.join(baseDir, 'lcov-report') }), | |
Report.create('lcovonly', { writer: writer, dir: baseDir }) | |
]; | |
utils.removeDerivedInfo(coverageObject); | |
collector.add(coverageObject); | |
res.statusCode = 200; | |
res.setHeader('Content-type', 'application/zip'); | |
res.setHeader('Content-Disposition', 'attachment; filename=coverage.zip'); | |
stream.pipe(res); | |
writer.writeFile('coverage.json', function (w) { | |
w.write(JSON.stringify(coverageObject, undefined, 4)); | |
}); | |
reports.forEach(function (report) { | |
report.writeReport(collector); | |
}); | |
writer.done(); | |
}); | |
//merge client coverage posted from browser | |
app.post('/client', function (req, res) { | |
var body = req.body; | |
console.log(opts); | |
if (!(body && typeof body === 'object')) { //probably needs to be more robust | |
return res.send(400, 'Please post an object with content-type: application/json'); | |
} | |
core.createNonTestedFilesCoverage(opts); | |
core.mergeClientCoverage(body); | |
res.json({ok: true}); | |
}); | |
return app; | |
} | |
function defaultClientMatcher(req) { | |
var parsed = url.parse(req.url); | |
return parsed.pathname && parsed.pathname.match(JS_RE); | |
} | |
function defaultPathTransformer(root) { | |
return function (req) { | |
var parsed = url.parse(req.url), | |
pathName = parsed.pathname; | |
if (pathName && pathName.charAt(0) === '/') { | |
pathName = pathName.substring(1); | |
} | |
return path.resolve(root, pathName); | |
}; | |
} | |
function clientHandler(matcher, pathTransformer, opts) { | |
var verbose = opts.verbose; | |
return function (req, res, next) { | |
if (!matcher(req)) { return next(); } | |
var fullPath = pathTransformer(req); | |
if (!fullPath) { return next(); } | |
if (!core.getInstrumenter()) { | |
console.error('No instrumenter set up, please call createHandler() before you use the client middleware'); | |
return next(); | |
} | |
if (!existsSync(fullPath)) { | |
console.warn('Could not find file [' + fullPath + '], ignoring'); | |
return next(); | |
} | |
fs.readFile(fullPath, 'utf8', function (err, contents) { | |
var instrumented; | |
if (err) { | |
console.warn('Error reading file: ' + fullPath); | |
return next(); | |
} | |
try { | |
instrumented = core.getInstrumenter().instrumentSync(contents, fullPath); | |
if (verbose) { console.log('Sending instrumented code for: ' + fullPath + ', url:' + req.url); } | |
res.setHeader('Content-type', 'application/javascript'); | |
return res.send(instrumented); | |
} catch (ex) { | |
console.warn('Error instrumenting file:' + fullPath); | |
return next(); | |
} | |
}); | |
}; | |
} | |
function createClientHandler(root, opts) { | |
opts = opts || {}; | |
var app = express(), | |
matcher = opts.matcher || defaultClientMatcher, | |
pathTransformer = opts.pathTransformer || defaultPathTransformer(root); | |
app.get('*', clientHandler(matcher, pathTransformer, opts)); | |
return app; | |
} | |
module.exports = { | |
createClientHandler: createClientHandler, | |
createHandler: createHandler, | |
hookLoader: core.hookLoader | |
}; | |
- That's all. Now if you run your tests, whichever files not loaded during test will also be covered in the coverage report having 0% coverage for those.
thanks for sharing this information for us
ReplyDeleteArtificial Intelligence training in Bangalore
Artificial Intelligence training in BTM
data science with python training in Bangalore
data science with python training in BTM
Blue Prism Training in Bangalore
Blue Prism Training in BTM
MERN StackTraining in Bangalore
MERN Stack Training in BTM