События testplane
Обзор
Как устроено описание событий
Ниже описаны все события testplane, на которые можно подписаться в своем плагине.
Описание каждого события начинается с тегов, которые представлены следующими вариантами:
- sync или async обозначают, соответственно, синхронный и асинхронный режимы вызова обработчика события;
- master обозначает, что данное событие доступно из мастер-процесса testplane;
- worker обозначает, что данное событие доступно из воркеров (подпроцессов) testplane;
- can be intercepted обозначает, что данное событие можно перехватить и соответственно, изменить.
Далее идут:
- описание обстоятельств, при которых триггерится событие;
- сниппет с кодом, показывающим как на него подписаться;
- параметры обраб отчика события;
- и опционально, примеры использования данного события в плагине или плагинах.
Схема генерации событий
Описание последовательности событий
Testplane можно запускать как через CLI (командную строку), так и через её API: из скрипта с помощью команды run.
После запуска testplane загружает все плагины и переходит к парсингу CLI, если она была запущена через CLI, или сразу к стадии инициализации, если её запустили через API.
Парсинг CLI
Если testplane была запущена через CLI, то она триггерит событие CLI. Любой плагин может подписаться на это событие, чтобы добавить свои опции и команды к testplane до того, как testplane распарсит CLI.
Если testplane была запущена с помощью API, то стадия парсинга CLI будет пропущена и сразу же начнется стадия инициализации.
Инициализация
Во время инициализации testplane триггерит событие INIT. Это событие срабатывает всего 1 раз за весь запуск testplane. Подписавшись на это событие, плагины могут выполнить всю необходимую им подготовительную работу: открыть и прочитать какие-то файлы, поднять dev-сервер, инициализировать структуры данных, и т. д. и т. п.
Затем testplane запускает подпроцессы (так называемые воркеры), в рамках которых будут выполняться все тесты. В мастер-процессе testplane тесты не выполняются, а только осуществляется общая оркестрация всего процесса запуска тестов, включая генерацию событий при завершении выполнения отдельных тестов.
Количество воркеров, которые запускает testplane, регулируется параметром workers в разделе system конфига testplane. При запуске очередного воркера testplane триггерит специальное событие NEW_WORKER_PROCESS.
Все тесты testplane запускает в воркерах, чтобы не столкнуться с ограничениями по памяти и CPU для мастер-процесса. Как только в воркере количество выполненных тестов достигнет значения testsPerWorker, воркер завершит свою работу, и будет запущен новый воркер. Соответственно, будет снова послано событие NEW_WORKER_PROCESS.
Чтение тестов
После этого testplane читает все тесты с файловой системы в мастер-процессе. Посылая для каждого файла, перед тем как его прочитать, событие BEFORE_FILE_READ и после прочтения — событие AFTER_FILE_READ.
После того, как все тесты будут прочитаны, триггерится событие AFTER_TESTS_READ.
Запуск тестов
Затем testplane посылает события RUNNER_START и BEGIN. И стартует новую сессию (браузерную сессию), в которой будут выполняться тесты. При старте сессии testplane триггерит событие SESSION_START.
Если количество тестов, выполненных в рамках одной сессии, достигнет значения параметра testsPerSession, то testplane завершит сессию, стриггерив событие SESSION_END, и запустит новую, послав событие SESSION_START.
Если тест при выполнении упадет с критической ошибкой, то testplane:
- досрочно удалит сессию и браузер, который с ней связан;
- создаст новую сессию;
- запросит новый браузер, и привяжет его к новой сессии.
Это нужно для того, чтобы сбой в сессии при выполнении одного из тестов не начал влиять на запуски последующих тестов.
После создания новой сессии testplane переходит к выполнению тестов. Все тесты выполняются в воркерах, но сам запуск и сбор результатов прогона тестов осуществляется в рамках мастер-процесса. Мастер-процесс триггерит событие SUITE_BEGIN для describe-блоков в файле с тестами и TEST_BEGIN для it-блоков. Если тест отключен с помощью хелперов типа skip.in и тому подобных, то триггерится событие TEST_PENDING.
Далее воркеры получают от мастер-процесса информацию о конкретных тестах, которые они должны запустить. Так как тесты хранятся в файлах, то воркеры читают конкретно те файлы, в которых находятся требуемые тесты. И перед чтением каждого такого файла, в каждом воркере триггерится событие BEFORE_FILE_READ, а после прочтения — событие AFTER_FILE_READ.
После того как соответствующие файлы с тестами будут прочитаны воркером, триггерится событие AFTER_TESTS_READ.
Перечисленные 3 события — BEFORE_FILE_READ, AFTER_FILE_READ и AFTER_TESTS_READ будут триггериться в воркерах в процессе прогона тестов каждый раз как воркеры будут получать от мастер-процесса очеред ные тесты, которые нужно запустить. Кроме случаев, когда соответствующий файл с тестами уже был прочитан воркером ранее. Так как после первого чтения какого-либо файла, воркер сохраняет его в кэше, чтобы в следующий раз избежать повторного чтения файла с тестами.
Потому что один файл может содержать множество тестов. А запуск тестов идет по отдельным тестам, а не по файлам. Поэтому в какой-то момент времени может прогоняться тест из файла, из которого уже прогонялся другой тест. В таких случаях кэширование защищает от ненужных повторных чтений одних и тех же файлов.
Прежде чем тест будет запущен, триггерится событие NEW_BROWSER. Однако это событие будет триггериться не для всех тестов, так как один и тот же браузер может использоваться много раз для запуска тестов (см. пара метр sessionsPerBrowser). Также в случае падения теста с критической ошибкой происходит пересоздание браузера, чтобы не допустить падения других тестов в этом браузере из-за системного сбоя. В этом случае снова будет послано событие NEW_BROWSER.
Завершение тестов
После того как тест завершается, может быть послано событие SESSION_END. Но это только в том случае, если суммарное количество тестов, которые были запущены в использованной сессии, превысило значение testsPerSession.
Далее всё будет зависеть от того, с каким результатом завершился прогон теста. Если тест прошел успешно, то testplane стриггерит событие TEST_PASS. Если тест упал — TEST_FAIL. Если тест упал, но его следует запустить повторно (см. настройки retry и shouldRetry в конфиге testplane), то вместо события TEST_FAIL будет отправлено событие RETRY.
Если тест не нужно запускать повторно, и результат — окончательный, то testplane триггерит события TEST_END и SUITE_END, если речь идет о завершении выполнения describe-блока.
После того как все тесты будут выполнены, а сессии завершены, testplane триггерит события END и RUNNER_END.
Обновление эталонных скриншотов
Во время запуска тестов может произойти обновление эталонных скриншотов. Это может произойти по следующим причинам:
- разработчик запустил testplane в специальном GUI-режиме и дал команду «принять скриншоты»;
- разработчик указал опцию
--update-ref
при запуске testplane; - у тестов не было эталонных скриншотов.
Во всех этих случаях триггерится событие UPDATE_REFERENCE.
Ошибки и аварийное завершение работы
Если во время прогона тестов, в одном из перехватчиков событий произойдет критическая ошибка, то testplane стриггерит для этого теста событие ERROR. При этом остальные тесты будут выполняться в штатном порядке.
Если во время прогона тестов, testplane получит сигнал SIGTERM (например, в результате нажатия клавиш Ctrl + C
), то testplane стриггерит событие EXIT и досрочно прервет выполнение тестов.
Про перехват событий
Testplane позволяет разработчику перехватывать ряд событий и изменять их на другие, игнорировать или задерживать их обработку.
События, которые могут быть перехвачены, снабжены в описании тегом can be intercepted. Всего таких событий 7:
Меняем одно событие на другое
Например, код ниже показывает, как можно перехватывать событие TEST_FAIL и изменять его на событие TEST_PENDING — то есть автоматически отключать падающие тесты, не давая им уронить общий прогон тестов:
module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
const test = Object.create(data);
test.pending = true;
test.skipReason = "intercepted failure";
return { event: testplane.events.TEST_PENDING, data: test };
});
testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик никогда не будет вызван
});
testplane.on(testplane.evenst.TEST_PENDING, test => {
// этот обработчик будет всегда вызываться вместо обработчика для TEST_FAIL
});
};
Оставляем событие as is
Если по какой-либо причине перехваченное событие нужно оставить как есть, то его обработчик должен вернуть точно такой же объект или любое falsey значение: undefined, null или false.
module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
return { event, data };
// или return;
// или return null;
// или return false;
});
testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик будет вызван как обычно,
// потому что перехват события TEST_FAIL никак не меняет его
});
};
Игнорируем событие
Чтобы проигнорировать какое-либо событие и не дать ему распространяться дальше, нужно вернуть из обработчика (в котором перехватывает ся событие) пустой объект:
module.exports = testplane => {
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
return {};
});
testplane.on(testplane.events.TEST_FAIL, test => {
// этот обработчик никогда не будет вызван
});
};
Задерживаем обработку события
Приведенный выше подход с игнорированием события можно использовать для задержки появления тех или иных событий, например:
module.exports = testplane => {
const intercepted = [];
testplane.intercept(testplane.events.TEST_FAIL, ({ event, data }) => {
// собираем все события TEST_FAIL
intercepted.push({ event, data });
// и не даем им распространяться дальше
return {};
});
testplane.on(testplane.events.END, () => {
// после окончания прогона тестов, триггерим все накопленные события TEST_FAIL
intercepted.forEach(({ event, data }) => testplane.emit(event, data));
});
};
Передача информации между обработчиками событий
События, которые триггерятся в мастер-процессе и в воркерах testplane не могут обмениваться информацией через глобальные переменные.
Например, такой подход работать не будет:
module.exports = testplane => {
let flag = false;
testplane.on(testplane.events.RUNNER_START, () => {
flag = true;
});
testplane.on(testplane.events.NEW_BROWSER, () => {
// будет выведено значение false, потому что событие NEW_BROWSER
// триггерится в воркере testplane, а RUNNER_START – в мастер-процессе
console.info(flag);
});
testplane.on(testplane.events.RUNNER_END, () => {
// будет выведено значение true
console.info(flag);
});
};
Но можно решить проблему следующим образом:
module.exports = (testplane, opts) => {
testplane.on(testplane.events.RUNNER_START, () => {
opts.flag = true;
});
testplane.on(testplane.events.NEW_BROWSER, () => {
// будет выведено значение true, потому что свойства в конфиге,
// которые имеют примитивный тип (а переменная "opts" является частью конфига),
// автоматически передаются в воркеры во время события RUNNER_START
console.info(opts.flag);
});
};
Или следующим образом: смотрите пример из описания события NEW_WORKER_PROCESS.
Параллельное выполнение кода плагина
У раннера тестов есть метод registerWorkers
, который регистрирует код плагина для параллельного выполнения в воркерах testplane. Метод принимает следующие параметры:
Параметр | Тип | Описание |
workerFilepath | String | Абсолютный путь к воркеру. |
exportedMethods | String[] | Список экспортируемых методов. |
При этом возвращает объект, который содержит асинхронные функции с именами из экспортированных методов.
Файл с путем workerFilepath
должен экспортировать объект, который содержит асинхронные функции с именами из exportedMethods
.
Пример параллельного выполнения кода плагина
Код плагина: plugin.js
let workers;
module.exports = testplane => {
testplane.on(testplane.events.RUNNER_START, async runner => {
const workerFilepath = require.resolve("./worker");
const exportedMethods = ["foo"];
workers = runner.registerWorkers(workerFilepath, exportedMethods);
// выводит FOO_RUNNER_START
console.info(await workers.foo("RUNNER_START"));
});
testplane.on(testplane.events.RUNNER_END, async () => {
// выводит FOO_RUNNER_END
console.info(await workers.foo("RUNNER_END"));
});
};
Код воркера: worker.js
module.exports = {
foo: async function (event) {
return "FOO_" + event;
},
};
CLI
sync | master
Событие CLI
триггерится сразу при запуске, до того, как testplane распарсит CLI. Обработчик события выполняется синхронно. С помощью него можно добавить новые команды или расширить справку testplane.
Подписка на событие
testplane.on(testplane.events.CLI, cli => {
console.info("Выполняется обработка события CLI...");
cli.option(
"--some-option <some-value>",
"the full description of the option",
// см. подробнее в https://github.com/tj/commander.js#options
);
});
Параметры обработчика
В обработчик события передается объект типа Commander.
Пример использования
Рассмотрим в качестве примера реализацию плагина @testplane/test-repeater.
Используя событие CLI, плагин добавляет к testplane новую опцию --repeat
. С помощью неё можно указать, сколько раз нужно прогнать тесты, независимо от результата каждого прогона.
Нажмите, чтобы посмотреть код
const parseConfig = require('./config');
module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);
if (!pluginConfig.enabled <tr><td>testplane.isWorker()) {
// или плагин отключен, или мы находимся в контексте воркера – уходим
return;
}
testplane.on(testplane.events.CLI, (cli) => {
// добавляем опцию --repeat
cli.option(
'--repeat <number>',
'how many times tests should be repeated regardless of the result',
(value) => parseNonNegativeInteger(value, 'repeat')
);
});
// ...
};
INIT
async | master
Событие INIT
триггерится перед тем, как будут выполнены задачи run или readTests. Обработчик события может быть асинхронным: в таком случае задачи начнут выполняться только после того, как будет разрезолвлен Promise, возвращаемый обработчиком события. Событие триггерится только один раз, независимо от того, сколько раз будут выполнены задачи.
Подписка на событие
testplane.on(testplane.events.INIT, async () => {
console.info("Выполняется обработка события INIT...");
});
Параметры обработчика
В обработчик события никакие данные не передаются.
Пример использования
В обработчике события INIT можно организовать, например, запуск dev-сервера для вашего проекта.
Dev-сервер — это express-like приложение, которое позволяет разрабатывать фронтенд проекта.
Ниже приведена самая короткая реализация. Более подробный пример можно посмотреть в разделе «Автоматический запуск dev-сервера».
Нажмите, чтобы посмотреть код
const http = require('http');
const parseConfig = require('./config');
module.exports = (testplane, opts) => {
const pluginConfig = parseConfig(opts);
if (!pluginConfig.enabled</td></tr> testplane.isWorker()) {
// или плагин отключен, или мы находимся в контексте воркера – уходим
return;
}
// ...
testplane.on(testplane.events.INIT, () => {
// контент, который отдает dev-сервер
const content = '<h1>Hello, World!</h1>';
// создаем сервер и начинаем слушать порт 3000
http
.createServer((req, res) => res.end(content))
.listen(3000);
// по адресу http://localhost:3000/index.html будет отдаваться: <h1>Hello, World!</h1>
});
};
BEFORE_FILE_READ
sync | master | worker
Событие BEFORE_FILE_READ
триггерится перед тем, как будет прочитан файл с тестом, чтобы распарсить его. Обработчик события выполняется синхронно. Событие также доступно в воркерах testplane.
Подписка на событие
testplane.on(testplane.events.BEFORE_FILE_READ, ({ file, testplane, testParser }) => {
testParser.setController("<some-command-name>", {
"<some-helper>": function (matcher) {
// ...
},
});
});
Параметры обработчика
В обработчик события передается объект следующего формата:
{
file, // String: путь к файлу с тестом
testplane, // Object: то же, что и global.testplane
testParser; // Object: типа TestParserAPI
}
testParser: TestParserAPI
Объект testParser
типа TestParserAPI
передается в обработчик события BEFORE_FILE_READ
. С помощью него можно управлять процессом парсинга файлов с тестами. Объект поддерживает метод setController
, с помощью которого можно создать свои хелперы для тестов.
setController(name, methods)
Метод добавляет контроллер к глобальному объекту testplane
, доступному внутри тестов.
name
— имя хелпера (или иначе — контроллера);methods
— объект-словарь, ключи которого задают названия методов соответствующих хелперов, а значения ключей определяют их реализацию. Каждый метод будет вызван на соответствующем тесте или наборе тестов (suite).
Контроллер будет удален, как только закончится парсинг текущего файла.
Пример использования
Создадим в качестве примера специальный хелпер testplane.logger.log()
, с помощью которого мы сможем логировать информацию о парсинге интересующего нас теста.
Нажмите, чтобы посмотреть пример использования
Код плагина
testplane.on(testplane.events.BEFORE_FILE_READ, ({ file, testParser }) => {
testParser.setController("logger", {
log: function (prefix) {
console.log(
`${prefix}: только что распарсил ${this.fullTitle()} из файла ${file} для браузера ${this.browserId}`,
);
},
});
});
Код теста
describe("foo", () => {
testplane.logger.log("some-prefix");
it("bar", function () {
// ...
});
});
Ещё один пример использования события BEFORE_FILE_READ можно посмотреть в разделе «Запуск тестов с хелперами».
AFTER_FILE_READ
sync | master | worker
Событие AFTER_FILE_READ
триггерится после того, как будет прочтен файл с тестом. Обработчик события выполняется синхронно. Событие также доступно в воркерах testplane.
Подписка на событие
testplane.on(testplane.events.AFTER_FILE_READ, ({ file, testplane }) => {
console.info("Выполняется обработка события AFTER_FILE_READ...");
});
Параметры обработчика
В обработчик события передается объект следующего формата:
{
file, // String: путь к файлу с тестом
testplane; // Object: то же, что и global.testplane
}
AFTER_TESTS_READ
sync | master | worker
Событие AFTER_TESTS_READ
триггерится после того, как будут вызваны методы readTests или run объекта типа TestCollection. Обработчик события выполняется синхронно. Событие также доступно в воркерах testplane.
Подписавшись на это событие, вы можете выполнить в обработчике те или иные манипуляции над коллекцией тестов до того, как они будут запущены. Например, вы можете исключить какие-либо тесты из запусков.