REserve — Testing UI5 — Executing tests

QUnit hooks

The OPA framework is a layer on top of QUnit. Developped by John Resig, the QUnit framework was originally designed to test jQuery. In 2008, it became a standalone project and, since, it is widely used.

  • QUnit.testDone : triggers a callback whenever a test ends
  • QUnit.done : triggers a callback whenever the test suite ends
(function () {
'use strict'
function post (url, data) {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/_/' + url)
xhr.send(JSON.stringify(data))
}
QUnit.begin(function (details) {
post('QUnit/begin', details)
})
QUnit.testDone(function (report) {
post('QUnit/testDone', report)
})
QUnit.done(function (report) {
post('QUnit/done', report)
})
}())

Injecting QUnit hooks

The only problem is to find a way to inject these hooks.

  • They are processed internally with the dispatch helper.
  • To avoid an infinite loop (and ensure that the UI5 resource is being retreived), the request ui5Request is flagged with a member internal set to true. The mapping ignores it when it loops back.
  • Once the internal responses are obtained, the final one is built by concatenating the two results.
const { Request, Response } = require('reserve')/*...*/{
// QUnit hooks
match: '/_/qunit-hooks.js',
file: join(__dirname, './inject/qunit-hooks.js')
}, {
// Concatenate qunit.js source with hooks
match: /\/thirdparty\/(qunit(?:-2)?\.js)/,
custom: async function (request, response, scriptName) {
if (request.internal) {
return // ignore to avoid infinite loop
}
const ui5Request = new Request('GET', request.url)
ui5Request.internal = true
const ui5Response = new Response()
const hooksRequest = new Request('GET', '/_/qunit-hooks.js')
const hooksResponse = new Response()
await Promise.all([
this.configuration.dispatch(ui5Request, ui5Response),
this.configuration.dispatch(hooksRequest, hooksResponse)
])
const hooksLength = parseInt(hooksResponse.headers['content-length'], 10)
const ui5Length = parseInt(ui5Response.headers['content-length'], 10)
response.writeHead(ui5Response.statusCode, {
...ui5Response.headers,
'content-length': ui5Length + hooksLength,
'cache-control': 'no-store' // for debugging purpose
})
response.write(ui5Response.toString())
response.end(hooksResponse.toString())
}
}

Endpoints

To collect information about each executed page, the runner associates an object with them. The qUnit hooks endpoinds are filling this object.

  • failed : the count of failed tests
  • passed : the count of passed tests
  • tests : an array aggregating the information reported by QUnit.testDone
  • report : the information reported by QUnit.done
const { promisify } = require('util')
const { writeFile } = require('fs')
const writeFileAsync = promisify(writeFile)
const { filename } = require('./tools')
/* ... */{
// Endpoint to receive QUnit.begin
match: '/_/QUnit/begin',
custom: endpoint((url, details) => {
job.testPages[url] = {
total: details.totalTests,
failed: 0,
passed: 0,
tests: []
}
})
}, {
// Endpoint to receive QUnit.testDone
match: '/_/QUnit/testDone',
custom: endpoint((url, report) => {
const page = job.testPages[url]
if (report.failed) {
++page.failed
} else {
++page.passed
}
page.tests.push(report)
})
}, {
// Endpoint to receive QUnit.done
match: '/_/QUnit/done',
custom: endpoint((url, report) => {
const page = job.testPages[url]
page.report = report
const reportFileName = join(job.tstReportDir, `${filename(url)}.json`)
const promise = writeFileAsync(reportFileName, JSON.stringify(page))
promise.then(() => stop(url))
})
}

Execution queue

Last but not least, the runner needs to sequence the execution of the tests.

  • testPagesCompleted : the number of tests completed. When this number equals the number of test pages, the runner knows that the tests are over.
/* ... */
server
.on('ready', ({ url, port }) => {
job.port = port
if (!job.logServer) {
console.log(`Server running at ${url}`)
}
extractTestPages()
})
async function extractTestPages () {
await start('/test/testsuite.qunit.html') // fills job.testPageUrls
job.testPagesStarted = 0
job.testPagesCompleted = 0
job.testPages = {}
for (let i = 0; i < job.parallel; ++i) {
runTestPage()
}
}
async function runTestPage () {
const { length } = job.testPageUrls
if (job.testPagesCompleted === length) {
// Last test completed
return generateReport()
}
if (job.testPagesStarted === length) {
return // No more tests to run
}
const index = job.testPagesStarted++
const url = job.testPageUrls[index]
await start(url)
++job.testPagesCompleted
runTestPage()
}
async function generateReport () {
/* ... */
}

Next step

The platform executes the tests. The next step is to measure code coverage.

--

--

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