В этой третьей статье средство выполнения улучшено, чтобы обеспечить выполнение тестов (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, это может быть:

Регулярное выражение /\/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 () {
  /* ... */
}

Последовательность тестов внутри бегуна

Следующий шаг

Платформа выполняет тесты. Следующий шагизмерение покрытия кода.