REserve — Testing UI5 — Measuring code coverage

Fundamentals

Before jumping to the implementation details, I would like to spend some time on fundamentals.

  • the code is relevant : when all expected features are tested, any code that is not triggered is useless.
function div (a, b) { return a / b; }
assert.strictEqual(div(4,2), 2);
  • div('1','b')
  • is div(1,3) * 3 equal to 1 ?
  1. Evaluation of the code
  2. Extraction and consolidation of measurement
  • While the code is executed, it keeps track of which functions, lines and conditions are evaluated.
function div (a, b) { return a / b; }
function cov_1scmxvb45(){var path="div.js";var hash="021d227dc28d530884d8c843c2806b96b01d5347";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path:"div.js",statementMap:{"0":{start:{line:1,column:22},end:{line:1,column:35}}},fnMap:{"0":{name:"div",decl:{start:{line:1,column:9},end:{line:1,column:12}},loc:{start:{line:1,column:20},end:{line:1,column:37}},line:1}},branchMap:{},s:{"0":0},f:{"0":0},b:{},_coverageSchema:"1a1c01bbd47fc00a2c39e90264f33305004495a9",hash:"021d227dc28d530884d8c843c2806b96b01d5347"};var coverage=global[gcv]||(global[gcv]={});if(!coverage[path]||coverage[path].hash!==hash){coverage[path]=coverageData;}var actualCoverage=coverage[path];{// @ts-ignore
cov_1scmxvb45=function(){return actualCoverage;};}return actualCoverage;}cov_1scmxvb45();function div(a,b){cov_1scmxvb45().f[0]++;cov_1scmxvb45().s[0]++;return a/b;}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImRpdi5qcyJdLCJuYW1lcyI6WyJkaXYiLCJhIiwiYiJdLCJtYXBwaW5ncyI6Inl6QkFlWTt5RkFmWixRQUFTQSxDQUFBQSxHQUFULENBQWNDLENBQWQsQ0FBaUJDLENBQWpCLENBQW9CLCtDQUFFLE1BQU9ELENBQUFBLENBQUMsQ0FBR0MsQ0FBWCxDQUFlIiwic291cmNlc0NvbnRlbnQiOlsiZnVuY3Rpb24gZGl2IChhLCBiKSB7IHJldHVybiBhIC8gYjsgfSJdfQ==

NYC

nyc is a command line wrapper for Istanbul, a JS code coverage tool.

const { join } = require('path')
const { fork } = require('child_process')
function nyc (...args) {
const childProcess = fork(join(__dirname, '../node_modules/nyc/bin/nyc.js'), args, {
stdio: 'inherit'
})
let done
const promise = new Promise(resolve => { done = resolve })
childProcess.on('close', done)
return promise
}

Instrumenting sources

The command nyc instrument is used.

  • covTempDir : temporary directory to store the instrumented files as well as the individual coverage files (defaulted to '.nyc_output')
  • covReportDir : directory where to store the coverage report (defaulted to 'coverage')
{
"all": true,
"sourceMap": false
}
{
"all": true,
"exclude": [
"!**/test/**"
],
"sourceMap": false
}
/* ... */
server
.on('ready', ({ url, port }) => {
job.port = port
if (!job.logServer) {
console.log(`Server running at ${url}`)
}
await instrument()
executeTests()
})

Replacing coverage context

By default, the object used to aggregate coverage information is stored at the window level.

const { promisify } = require('util')
const { readdir, readFile, stat } = require('fs')
const readdirAsync = promisify(readdir)
const readFileAsync = promisify(readFile)
const statAsync = promisify(stat)
const { Readable } = require('stream')
const globalContextSearch = 'var global=new Function("return this")();'
const globalContextReplace = 'var global=window.top;'
const customFileSystem = {
stat: path => statAsync(path)
.then(stats => {
if (stats) {
stats.size -= globalContextSearch.length + globalContextReplace.length
}
return stats
}),
readdir: readdirAsync,
createReadStream: async (path) => {
const buffer = (await readFileAsync(path))
.toString()
.replace(globalContextSearch, globalContextReplace)
return Readable.from(buffer)
}
}

Replacing sources with instrumented files

Now that the instrumented files are generated and the coverage information is stored at the right place, a new mapping is created to substitute the source files with the instrumented ones. To work properly, it must be inserted before the source mapping.

{
match: /^\/(.*\.js)$/,
file: join(instrumentedSourceDir, '$1'),
'ignore-if-not-found': true,
'custom-file-system': customFileSystem
}

Extracting the code coverage

When the test page ends, the QUnit.done callback is triggered. This is the perfect time to collect the generated coverage information.

/* Injected QUnit hooks */
(function () {
'use strict'
function post (url, data) {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/_/' + url)
xhr.send(JSON.stringify(data))
}
/* ... */ QUnit.done(function (report) {
if (window.__coverage__) {
report.__coverage__ = window.__coverage__
}
post('QUnit/done', report)
})
}())
{
// Endpoint to receive QUnit.done
match: '/_/QUnit/done',
custom: endpoint((url, report) => {
const page = job.testPages[url]
const promises = []
if (report.__coverage__) {
const coverageFileName = join(job.covTempDir, `${filename(url)}.json`)
promises.push(writeFileAsync(coverageFileName, JSON.stringify(report.coverage__)))
delete report.__coverage__
}
page.report = report
const reportFileName = join(job.tstReportDir, `${filename(url)}.json`)
promises.push(writeFileAsync(reportFileName, JSON.stringify(page)))
Promise.all(promises).then(() => stop(url))
})
}

Generating the code coverage reports

When all the test pages are executed, the coverage report is generated using two commands :

  • nyc report to generate an HTML report summarizing the coverage information in the covReportDir directory.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store