Тестирование в разработке — вещь настолько привычная, что о его важности обычно даже не спорят. Если продукт развивается, появляются новые функции, интерфейсы меняются, пользовательские сценарии усложняются — без тестирования всё это быстро превращается в хаос.
А вот пользовательская документация живёт ��уда более сложной жизнью. Её либо не пишут вовсе, надеясь на хороший UX, либо пишут один раз и больше к ней не возвращаются. Через несколько месяцев оказывается, что интерфейс уже другой, кнопки переехали, а скриншоты выглядят как привет из прошлого релиза.
В этой статье я хочу показать подход, при котором автоматизированные e2e-тесты используются не только для проверки работоспособности продукта, но и для автоматического создания актуального пользовательского руководства — на базе реальных сценариев и реального интерфейса.
Где вообще место пользовательской документации
Если разложить разработку продукта на этапы, получится примерно такая схема (упрощённая, но показательная):
Салфеточный прототип — начальная идея об продукте, которая просто в общих чертах описывает то, что мы придумали и хотим попробовать реализовать
Аналитика и Бизнес-документация — здесь мы уже проанализировали идею и подробно описали, как вообще эта штука работает; бывает, что на этом пункте процесс заканчивается, т.к. оно не рентабельно / не интересно / нерешаемо
Дизайн — в этот момент к нам приходят магистры и феи Фигмы, реализовав в интерфейсах всё, что было придумано ранее
Разработка — место, где ожидания превращаются в реальность, или не превращаются (тут как повезёт)
Тестирование — здесь мы сравниваем то, что было запланировано с тем, что получилось, и главное, чтобы всё ещё нормально работало
Релиз — отправляем этот корабль в плавание
Об��луживание — следим за кораблём, добавляем новые возможности и чиним при необходимости
Первые этапы сразу отпадают: на них просто нечего показывать пользователю. Дизайн — вариант получше, но итоговый интерфейс частенько не совпадает с макетами на 100%. И тут даже дело не в том, что на этапе разработки ожидание и реальность так и не сошлись. Бывает прощё — в дизайне на какой-то условной карточке было название из двух слов, а описание из десяти. А по факту пользователи название меньше, чем из 10 слов, даже не считают достойным быть — и по-другому никак.
Релиз — это вообще отдельное таинство доставки кода до пользователей.
Пост-релизное обслуживание будто бы подходит — продукт уже выпущен и можно даже написать это самое руководство, как это обычно и делается. Да, это требует времени, но актуализация документации тоже важная задача.
А вот тестирование я упустил нарочно, ибо по правилам повествования оно должно быть именно здесь, чтобы не разрывать ход мысли 😎. В хороших командах новые функции всё равно проходят через e2e-сценарии, которые повторяют реальные действия пользователя. И если мы уже открываем страницы, нажимаем кнопки, заполняем формы и проверяем результат, то почему бы не использовать этот процесс ещё и для создания пользовательского руководства?
Идея подхода
Ключевая мысль простая:
Один пользовательский сценарий = один тест + один фрагмент документации
Тест проверяет, что функциональность работает, а параллельно:
фиксирует шаги
делает скриншоты интерфейса
формирует читаемое описание действий пользователя.
Таким образом, документация всегда соответствует текущему состоянию продукта — ровно потому, что она создаётся тем же кодом, что и тесты.
Практика: небольшой MVP
Чтобы не витать в абстракциях, я взял своё небольшое приложение — Tasker.app, простой менеджер задач без базы данных.
В качестве инструмента для автоматизации использую Puppeteer. Это не принципиально — подойдёт любая библиотека для e2e-тестирования, но с Puppeteer я уже работал, поэтому выбор пал на него.
В итоге я хочу получить:
проверку реальных пользовательских сценариев
отдельную «статью» на каждый сценарий
набор статичных HTML-страниц с изображениями, которые легко опубликовать или доработать.
Пусть наша система называется FlowDocs.
Ядро системы
Начнём с главного — установки самой библиотеки:
npm i puppeteerИ заодно сразу создадим файл index.js, который будет корневым для нашего приложения. Пока оставим его пустым, лишь сразу, чтобы не возвращаться, добавим в package.json команду для старта:
"scripts": {
"start": "node index.js"
}Так как сценариев будет много, всю работу с браузером выносим в отдельный модуль. Например, функция клика по элементу:
/**
* Нажатие на элемент по ID
*/
async function clickById(page, elementId, timeout = 5000) {
try {
await page.waitForSelector(`#${elementId}`, { timeout });
await page.click(`#${elementId}`);
return true;
} catch (error) {
console.warn(`clickById: элемент #${elementId} не найден`, error.message);
return false;
}
}Подобных функций набирается довольно много — открытие страниц, ожидания, скриншоты и т.д. Да, весь код приводить здесь не буду — его накопилось изрядно — он будет доступен в этом репозитории, там можно будет всё изучить подробно.
Генерация документации
Для формирования итогового руководства используется отдельный класс Reporter. Он отвечает за:
заголовки
текст
изображения
ссылки
генерацию HTML
Упрощённая структура выглядит так:
class Reporter {
addHeader(text, level = 2) {}
addParagraph(text) {}
addLink(url, text) {}
addImage(image, description = '') {}
generateHTML(reportName = 'test-report') {}
async save(reportName) {}
clear() {}
}Для MVP этого достаточно, а при необходимости функциональность легко расширяется.
Экшены и повторяющиеся паттерны
Теперь поговорим про конкретное тестируемое приложение и подумаем — если ли у нас какие-то повторяющиеся паттерны в действиях, которые мы хотим так же вынести в одну функцию? Если да, то логично добавить экшены, которые будут комбинировать стандартные функции. Например, модальные окна:
async function getModalWindow(page, className, timeout = 5000) {
try {
await page.waitForSelector(`.${className}`, { timeout });
const result = await page.evaluate((selector) => {
const element = document.querySelector(selector);
if (!element) return null;
const parent = element.parentElement;
const { x, y, width, height } = element.getBoundingClientRect();
return {
id: parent.id,
position: { x, y, width, height }
};
}, `.${className}`);
return {
...result,
async setInput(input, text) {
await fillFieldById(page, `${result.id}-${input}`, text);
}
};
} catch {
return null;
}
}В результате сценарии становятся компактнее и читаемее.
Пользовательские сценарии
Каждый сценарий — это:
тест
будущая глава пользовательского руководства
Пример фрагмента сценария создания задачи:
reporter.addHeader('Создание новой задачи', 1);
reporter.addParagraph('Шаг 1: Открываем главную страницу приложения');
await loadPage(page, 'https://anatolykulikov.ru/app/tasker/');
await takeScreenshot(page, 'initial-page.png', { dir: reportName });
reporter.addImage('initial-page.png');Или работа с модальным окном:
const modalCreate = await getModalWindow(page, 'dialog');
const { position } = modalCreate;
reporter.addParagraph('Шаг 3: Откроется окно добавления задачи');
await takeScreenshot(page, 'modal-window.png', {
clip: createClipWithPadding(position),
dir: reportName
});
reporter.addImage('modal-window.png');В результате сценарий одновременно проверяет функциональность, фиксирует интерфейс и формирует понятную пошаговую инструкцию.
Финальная сборка
В самом начале мы оста��или одиноким index.js — пришло время наполнить и его. Поскольку это наша центральная точка, то здесь мы будем формировать главную страницу руководства, а также запускать те сценарии, которые нам нужны.
Запускаем процесс самой простой командой npm start (или можно node index.js, в нашем случае это одно и то же) и просто наблюдаем за процессом.
Итоги
В итоге получился процесс, который решает сразу две задачи: проверяет, что приложение работает так, как задумано, и параллельно формирует актуальное пользовательское руководство. Без отдельного этапа «написать документацию» и без постоянной ручной актуализации.
Описанный подход — это, по сути, MVP. Его можно развивать дальше: подключать CI/CD, автоматически публиковать документацию после релизов, добавлять версионирование или локализацию. Всё это ложится на уже существующую инфраструктуру тестирования и не требует отдельного процесса.
Это не серебряная пуля и не замена полноценной технической документации. Но для пользовательских сценариев, которые всё равно проходят через e2e-тесты, такой подход выглядит вполне разумным компромиссом между затратами и пользой.
