В этой третьей статье средство выполнения улучшено, чтобы обеспечить выполнение тестов (qUnit и OPA). Веб-сервер модифицируется для внедрения перехватывающих скриптов, и предоставляются новые конечные точки для получения результатов тестов. Кроме того, реализована базовая очередь выполнения, чтобы мы могли контролировать количество экземпляров, которые выполняются одновременно.
Хуки QUnit
Структура OPA — это слой поверх QUnit. Разработанная John Resig, инфраструктура QUnit изначально предназначалась для тестирования jQuery. В 2008 году он стал самостоятельным проектом и с тех пор широко используется.
Из-за своей популярности библиотека предлагает множество функций и, в частности, предоставляет некоторые хуки для отслеживания выполнения тестов:
- QUnit.begin: запускает обратный вызов всякий раз, когда начинается набор тестов.
- QUnit.testDone: запускает обратный вызов всякий раз, когда заканчивается тест.
- QUnit.done: запускает обратный вызов всякий раз, когда набор тестов завершается.
Каждый хук предоставляет информацию о текущем событии. Например, когда набор тестов начинается, объект, содержащий количество тестов для выполнения (элемент totalTests
), передается обратному вызову. Точно так же, когда тест заканчивается, параметр содержит информацию о количестве пройденных (участник passed
) и не прошедших (участник failed
) утверждений. .
Поскольку мы запускаем тесты, запуская браузер с URL-адресом тестовой страницы, можно отслеживать выполнение, используя эти перехватчики.
Используя выделенные конечные точки, мы можем отправить эту информацию исполнителю.
(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) }) }())
Перехватчики QUnit для отслеживания выполнения тестов
Внедрение хуков QUnit
Единственная проблема заключается в том, чтобы найти способ внедрить эти хуки.
Реализованное решение близко к замене скрипта, но с изюминкой: когда тестовая страница запрашивает ресурс qUnit, мы конкатенируем его с хуками.
Эта часть сложна и включает в себя различные механизмы, предлагаемые REserve.
Прежде всего, ресурс qunit является частью доставки UI5, это может быть:
- версия 1: https://openui5.hana.ondemand.com/resources/sap/ui/ ThirdParty/qunit.js
- версия 2: https://openui5.hana.ondemand.com/resources/sap/ui/ ThirdParty/qunit-2.js
Регулярное выражение /\/thirdparty\/(qunit(?:-2)?\.js)/
соответствует обоим.
Затем REserve может опубликовать любой локальный файл с помощью обработчика file
. Ленивый я не хочет читать файл с помощью fs API, поэтому объявлено сопоставление, чтобы сделать источник хуков qunit доступным по URL-адресу /_/qunit-hooks.js
.
Наконец, когда запрос достигает thirdparty/qunit.js
или thirdparty/qunit-2.js
:
- Создаются два новых запроса: один для получения ресурса qUnit (
ui5Request
, используется тот же URL), а другой для чтения хуков (hooksRequest
на/_/qunit-hooks.js
). - Они обрабатываются внутри с помощью
dispatch
помощника. - Чтобы избежать бесконечного цикла (и убедиться, что ресурс UI5 извлекается), запрос
ui5Request
помечен флагом с элементомinternal
, для которого установлено значениеtrue
. Отображение игнорирует его, когда оно возвращается назад. - После получения внутренних ответов окончательный создается путем объединения двух результатов.
Это сопоставление должно быть применено перед сопоставлением UI5.
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()) } }
Сопоставления для внедрения хуков в ресурс qunit
Конечные точки
Чтобы собрать информацию о каждой выполненной странице, бегун связывает с ними объект. Конечные точки перехватчиков qUnit заполняют этот объект.
Члены:
total
: общее количество тестовfailed
: количество неудачных тестовpassed
: количество пройденных тестовtests
: массив, объединяющий информацию, сообщаемую QUnit.testDone.report
: информация, сообщенная QUnit.done
По завершении тестов полученный объект сериализуется для последующего повторного использования. После создания файла браузер закрывается с помощью stop
API.
В задание добавлен новый параметр:
tstReportDir
: каталог, в котором хранятся отчеты
Важно подождать, пока файл будет записан, перед вызовом
stop
. Действительно, при вызовеstart
возвращается обещание, и это обещание разрешается сразу после вызоваstop
. Последняя остановка сигнализирует об окончании всех тестов, что означает, что программа запуска, возможно, получит доступ к этим файлам.
ПРИМЕЧАНИЕ. Помощник filename
преобразует URL-адрес в допустимое имя файла.
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)) }) }
Конечные точки, которые отслеживают выполнение тестов
Очередь выполнения
И последнее, но не менее важное: исполнителю необходимо упорядочить выполнение тестов.
В задание добавлен новый параметр:
parallel
: разрешенное количество параллельных тестов (по умолчанию2
)
Как уже объяснялось в предыдущей статье, после запуска встроенного сервера runner запускается проверка тестов. Получив список страниц для выполнения, бегун запускает количество тестов, заданное параметром parallel
.
Функция runTestPage
является своего рода рекурсивной, которая вызывает себя после завершения теста.
Для отслеживания прогресса добавлены два участника задания:
testPagesStarted
: количество уже запущенных тестов. Это также помогает узнать, какая страница должна быть запущена следующей.testPagesCompleted
: количество выполненных тестов. Когда это число равно количеству тестовых страниц, исполнитель знает, что тесты закончены.
Из-за ловушек qUnit конец теста остановит браузер, который разрешит обещание. Это означает, что после запуска браузера поток событий позаботится обо всем остальном, объясняя, почему код такой простой.
/* ... */ 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 () { /* ... */ }
Последовательность тестов внутри бегуна
Следующий шаг
Платформа выполняет тесты. Следующий шаг — измерение покрытия кода.