Тестирование в разработке — вещь настолько привычная, что о его важности обычно даже не спорят. Если продукт развивается, появляются новые функции, интерфейсы меняются, пользовательские сценарии усложняются — без тестирования всё это быстро превращается в хаос.
А вот пользовательская документация живёт куда более сложной жизнью. Её либо не пишут вовсе, надеясь на хороший 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-тесты, такой подход выглядит вполне разумным компромиссом между затратами и пользой.
