
При тестировании распределенных систем разработчики сталкиваются с асинхронным взаимодействием с серверами, громоздкими сценариями отправки и сложным входом для новичков. Это приводит к ошибкам, долгой отладке и росту затрат.
Привет, Хабр! Меня зовут Максим Попов, я инженер по автоматизированному тестированию внутренних продуктов в Сбере — в том числе SCPL. В этой статье расскажу, как упростить настройку клиент-сервер взаимодействия в рамках фреймворка автотестирования.
Что такое SCPL

SCPL — это омниканальная коммуникационная платформа для внутренних контактных центров Сбера. Она обеспечивает:
маршрутизацию звонков;
настройку сервисов для операторов;
экранные записи и запись разговоров;
возможность вести текстовые чаты и обрабатывать задачи.
Главный акцент в системе — рабочее место оператора. Именно оттуда специалист принимает и инициирует звонки, общается в чатах и решает другие задачи — все это в одном окне.
При этом каждое действие в интерфейсе связано с отправкой сообщений по WebSocket-протоколу, которые запускают логику во всей системе.
Разберем на примере входящих звонков:
Оператор меняет статус на «Готов» — отправляется WebSocket-сообщение.
Поступает вызов — система инициирует входящий звонок.
Оператор принимает звонок — отправляется подтверждение.
Во время разговора подключаются разные функции — например, удержание звонка или включение микрофона.
Завершение разговора — оператор закрывает сессию и оценивает звонок.
Каждое из этих действий — отдельное сообщение, которое система должна правильно обработать и дать соответствующий ответ.
Что такое WebSocket
Это протокол, который создает двустороннее постоянное соединение между клиентом и сервером. Он работает в real-time и мгновенно передает данные, удерживая сокет открытым все время работы.
На скриншоте видно разницу между HTTP и WebSocket. Если в HTTP все строго: запрос → ответ, то в WebSocket может быть иначе: один запрос → несколько ответов, несколько запросов → один ответ, ответ без запроса — все зависит от вашей системы.

Как мы используем WebSocket в SCPL
Разберем пример с исходящим звонком — чтобы его инициировать, нужно отправить три WebSocket-сообщения:
NewOutInteraction
;StartNewOutInteraction
;AcceptRTC
.
В ответ сервер возвращает результаты:
ResultsNewOutInteraction
;ResultsAcceptRTC
;ResultsStartNewOutInteraction
.
И здесь есть особенность — хотя StartNewOutInteraction
отправляется вторым, соответствующий ответ (ResultsStartNewOutInteraction
) приходит последним. Это нарушает ожидаемую последовательность и усложняет клиентскую логику.
Чтобы понять этот процесс, разберём его на уровне тел сообщений.

Первое сообщение — NewOutInteraction
– инициация исходящего звонка (1). В ответ мы получаем interactionID
и workItemID
(2). Эти значения нужно сохранить, поэтому workItemID
вставляется в следующее сообщение — StartNewOutInteraction
(3). Но пока не отправится AcceptRTC
, сервер не вернет ответ на StartNewOutInteraction
. В итоге запросы и ответы идут не «друг за другом» — это усложняет тестирование.
Когда результат поступил для NewOutInteraction, отправляется AcceptRTC
(4). Следующий шаг — результат для AcceptRTC
(5), который указывает на успешное принятие и обработку этого запроса. Далее получаем окончательный результат StartNewOutInteraction
(6) – это сигнализирует об успешном запуске исходящего звонка.
Чтобы покрыть такие сценарии, нужен простой и расширяемый инструмент, который обеспечит:
отправку цепочек сообщений в строгом порядке. Даже если сервер отвечает в произвольной последовательности, система должна сохранять логику бизнес-сценария.
сохранение и повторное использование данных. К примеру,
workItemID
иinteractionID
из одного запроса подставляются в следующие.асинхронную работу с WebSocket. Отправка и чтение сообщений должны выполняться параллельно, чтобы не мешать друг другу.
множественные подключения. В случае SCPL — это колл-центр. Несколько операторов работают одновременно, передают звонки и взаимодействуют между собой. Тесты должны это учитывать.
Хотя это в основном про WebSocket, такой подход применяют и в REST API — просто без асинхронности. Его основные принципы:
вызов серии REST-запросов для достижения состояния A → B;
парсинг ответов и автоматическая подстановка нужных данных в последующие вызовы;
упрощение логики — разработчик описывает сценарий, а фреймворк сам управляет взаимодействием с сервером.
Также мы пытались найти готовые решения — например, на Хабр, Mail,ru и Stack Overflow — но ничего не вышло. Обычно в них все сводится к базовой схеме работы с WebSocket — при открытии соединения клиент сразу шлет все сообщения, а ответы принимает без разбора.

Но такая реализация не решает ни одной из наших задач. Кроме того, у нее есть другие критичные недостатки:
Нет ясности, что отправлять. Чтобы перевести систему в состояние B, нужно знать, какие сообщения отправить. Это может быть огромная сложная цепочка сообщений, с которой, может быть лень разбираться Стандартный клиент ее не составит, поэтому приходится разбираться вручную.
Нет контроля ожиданий. WebSocket — асинхронный протокол. Иногда нужно приостановить отправку и дождаться данных, вроде
interactionID
. Примитивный клиент это не умеет — он шлет все подряд и не реагирует на условия.Нет обработки сообщений. Непонятно, как обрабатывать огромную разновидность сообщений на вход и на выход.
Поэтому мы остановились на собственной реализации, которая не имеет особых требований — все реализуется на Python через стандартную библиотеку asyncio. Также можно использовать gevent и запускать задачи чтения и отправки в greenlet-потоках.
Архитектура фреймворка и её проблемы
Теперь об архитектуре нашего подхода, и сразу — о типовых проблемах, с которыми можно столкнуться при реализации асинхронных WebSocket-клиентов.
Проблема №1: Вариативность сценариев
Тестовые сценарии в SCPL бывают сложными и непредсказуемыми. Вот один из простых, но реальных примеров работы:
оператор меняет статус на «Готов»;
затем на «Не готов»;
снова на «Готов»;
делает вызов;
завершает звонок;
снова отправляет сообщение.
Каждое бизнес-действие может скрывать за собой сколько угодно WebSocket-сообщений — здесь легко запутаться, особенно если сценариев десятки.
Решение:
Упростить сценарии, сделать их понятными в описании в коде и повторяемыми в тестировании.
Проблема №2: Сложный порог входа для новичков
Когда стажеру нужно написать тест с отправкой 20 WebSocket-сообщений в правильном порядке — задача выглядит пугающе. Документация не помогает — в ней описаны только типы сообщений, а не логика их применения.
Решение:
Чтобы облегчить процесс, делим сообщения на два уровня:
сценарные — описывают логику теста верхнеуровнево.
технические — то, что реально отправляется на WebSocket.
Теперь вместо ручного создания цепочки NewOutInteraction
→ StartNewOutInteraction
→ AcceptRTC
используется одно сценарное сообщение — OutboundCall
.

Такой сценарий строится в несколько этапов:
-
Подготовка шаблонов. У каждого проекта свои уникальные сообщения. Например,
StartNewOutInteraction
имеет пустые поляdestination
иworkItemID
(1). При использовании сценарного сообщения, вродеoutbound_call(destination)
, эти поля автоматически подставляются (2, 3), упрощая создание и отправку сообщений.
-
Подстановка значений. Например, значение для
destination
(куда звонить) (3) передается заранее, аworkItemID
, полученное от сервера, автоматически добавляется в нужные поля уже во время работы ws-клиента. На этапе формирования сценарного сообщения (1-5), технические добавляются в необходимом порядке, в них добавляется все, что известно до запуска ws-клиента (3).
Объединение технических сообщений в цепочки. Низкоуровневая механика скрывается за одним сценарным сообщением. Этот принцип применим не только к звонкам, но и ко всем бизнес-юнитам.
Так, в сценарном сообщении OutboundCall
сразу указывается все, что известно до запуска клиента — например, поле destination
(номер телефона для вызова). Параметры сценария можно хранить в словаре или прямо прописывать в JSON внутри теста.
Но возникает вопрос: как одно сценарное сообщение превращается в три технических — NewOutInteraction
, StartNewOutInteraction
, AcceptRTC?

Для этого реализован класс-преобразователь — он обращается к нужному обработчику по имени сценарного сообщения. Для OutboundCall
работает свой обработчик (2), который:
принимает входное событие (event) со всеми доступными данными;
формирует три отдельных сообщения;
фиксирует всё в логах — например, «Добавлено сообщение OutboundCall → сформированы и вставлены следующие WebSocket-сообщения: …»;
записывает информацию о выполнении сценария в логи.
Логи упрощают отладку и позволяют проводить аудит позже. В результате весь сценарий можно описать как простой читаемый набор шагов:
Переводим оператора в статус «Готов».
Делаем
OutboundCall
.Завершаем вызов (
HangUp
).
Каждый шаг — это сценарное сообщение. Под капотом оно может превращаться во много технических WebSocket-сообщений. Но в тесте этого не видно, и именно это делает его читаемым и удобным.
Так нам удалось решить сразу несколько проблем:
сложный вход для новичков. Теперь не нужно знать, что отправлять — достаточно понимать действия оператора.
вариативность сценариев. Сложные цепочки строятся простым добавлением шагов:
set_ready_status
→outbound_call
→agent_hangup
.гарантия порядка. WebSocket-сообщения всегда отправляются в правильной последовательности.
Проблема №3: Регулярное появление нового функционала
Платформа развивается постоянно. Появляются:
новые сообщения (
MessageA
,MessageB
);новые ответы от сервера (
ResultA
,ResultB
);новые логики обработки и зависимости между ними.
Когда все прописано вручную, код превращается в хаос из методов и if
-блоков. Поэтому нужна система, которая автоматически обрабатывает новые элементы и структурировано хранит все данные.
Решение:
Для отправляемых и получаемых сообщений применили паттерн «фабрика». Вот как это работает:
по имени сообщения выбирается соответствующий обработчик;
он сам решает, какой алгоритм применить;
фабрики обрабатывают сообщения серверу и от него
В итоге получается единообразная система, которую легко расширять. Например, если добавилось новое сообщение — его достаточно зарегистрировать в фабрике.
Теперь объясню, почему мы выбрали именно фабрику:
Инкапсуляция логики. Для каждого сообщения в фабрике указываются правила обработки, алгоритм выполнения и поля для извлечения/подстановки. Если сообщение можно обработать уже существующим алгоритмом, новый код писать не нужно — просто указываем правило повторного использования.
Гибкость. Фабрики разделяются по назначению — одна управляет отправкой, другая — приемом. При этом все обработчики лежат централизованно и в коде легко ориентироваться.
А добавление новой логики сводится к простым шагам — создаём константу сценарного сообщения, привязываем её к нужному обработчику и регистрируем в фабрике.
Вот как на практике выглядит отправка сообщений:
асинхронная задача вызывает фабрику и передает туда сообщение;
фабрика по имени сообщения выбирает обработчик из хеш-таблицы;
обработчик применяет алгоритм к сообщению, например, вставляет нужные данные —
interactionID
,workItemID
, или сохраняет их;в лог записывается информация об отправке или получении сообщения;
Пример

Фабрика получает событие (StartNewOutInteraction
) (1), находит обработчик (2), вставляет сохраненный workitem_state_id
(6) и отправляет сообщение на сервер. В логи пишется — «Отправлено: StartNewOutInteraction, вставлен workItemID, отправка завершена». После этого, задача с отправкой и обработкой завершается.
А так происходит приём сообщений:
асинхронная задача на чтение получает сообщение и передает его в фабрику;
фабрика выбирает обработчик;
обработчик парсит JSON и извлекает поля —
interactionID
,workItemID
;полученные данные сохраняются во внутреннем хранилище клиента.
Пример

Сообщение NewWorkItemState
попадает в обработчик (1). Там гарантированно есть поля interactionID
и workItemID
. Они извлекаются и сохраняются во внутреннем объекте клиента (2). После этого фабрика проставляет флаг waitFlag = true
, сигнализируя, что данные доступны (3).
Таким образом, фабрика автоматически реагирует на новые события и сохраняет только полезные данные.
В итоге, подход полностью закрыл проблему постоянного появления нового функционала. Если нужно добавить обработку нового сообщения — создаем обработчик. Если подходит старый алгоритм — просто указываем его напротив имени сообщения в хеш-таблице.
Проблема №4: Отправка сообщения только после получения нужных данных
Если отправить сообщение раньше, чем пришел interactionID
или workItemID
, возникнет ошибка. Поэтому системе нужен механизм, который дает сигнал «Ждём данные» — и только потом продолжает выполнять сценарий.
Решение:
Ранее уже упоминалось, что фабрика при получении сообщения проставляет waitFlag = true
, когда необходимые данные получены. Но кто этот флаг отслеживает? Этим занимается сам WebSocket-клиент — он запускает две асинхронные задачи:
отправку сообщений;
чтение сообщений.
Если на очереди сообщение, которое требует ожидания — например, зависит от interactionID
— задача на отправку блокируется, а на чтение —продолжает работать. Отправка возобновляется только после того, как waitFlag
меняет значение. Если в установленное время этого не произошло — срабатывает таймаут и клиент завершает свою работу.
В коде это выглядит так:

В
message_sender
(задача на отправку) поступает список технических сообщений, преобразованных из сценарных (1).Задача отправки берёт следующее сообщение из списка (2).
Если оно входит в список ожидания — например,
waitForInteraction
,waitOutboundConnect
, — то проверяется соответствующий флаг (3).Если флага нет, то выполняется ожидание по таймауту (4).
Если есть, то обработка продолжается.
После обработки флаг сбрасывается в исходное значение, чтобы следующий цикл ожидания отработал корректно (5).
Вот как выглядит чтение сообщений:

Так мы реализовали полностью асинхронное выполнение сценариев, гарантированное ожидание нужных данных и защиту от преждевременной отправки.
Что мы получили на практике
Описанный бизнес-сценарий легко переводится в технический список, который удобно обрабатывается клиентом. Такой подход дает сразу несколько преимуществ, которые делают работу быстрее и удобнее.
Генерация данных без кода
Даже тем, кто не пишет тесты, стало проще работать с системой. Разработчики или аналитики могут попросить:
«Нужно сделать 100 звонков на таком-то стенде и таком-то номере».
Все, что требуется — создать простую джобу, где указать:
стенд;
количество звонков;
номер.
Внутри скрыта вся работа WebSocket-механизма. Для пользователя это выглядит так — оператор 100 раз позвонил, взял трубку и положил ее. Это удобно и экономит массу времени — особенно для тех, кто никогда не писал код и не хочет этого делать.
Вызов функций во время сценария
Можно сделать обработчик на вызов синхронной функции во время работы асинхронного клиента:
функция выполняется стабильно;
не блокирует отправку и прием сообщений;
возвращаемые значения сохраняются и доступны после выполнения сценария.
Без этого пришлось бы вводить потоки и подгадывать время слипами. С таким обработчиком все работает без костылей — достаточно передать имя функции и аргументы (args
, kwargs
). Когда процесс дойдет до этого сценарного сообщения, фабрика вызовет функцию в отдельном потоке, не прерывая процессы.
Удобный дебаг
Все полученные сообщения сохраняются в папке temp/. С каждым тестом она очищается и туда складываются новые данные. Чтобы не было конфликтов, к имени добавляется индекс. Так можно:
проверить, на каком шаге началась проблема;
понять, кто виноват — сервер или разработчик теста;
восстановить последовательность и валидировать структуру.
Также в обработчиках сообщений можно оставлять уникальные логи:
какие данные вставлены;
какое сообщение отправлено;
что получено;
из чего трансформировано.
Это облегчает анализ и поиск ошибок.
Проверка формата сообщений
Все входящие сообщения дополнительно проверяются через библиотеку Pydantic:
Пишется модель данных.
Проверяется корректность полей, значений и общей структуры ответа.
Система становится надежнее и позволяет выявлять ошибки в форматах на раннем этапе.
Все эти возможности делают тестирование SCPL не только стабильным, но и удобным для любых участников команды — от стажеров до аналитиков.
Заключение
В результате мы получили инструмент, который обрабатывает каждое отправленное и полученное сообщение, но при этом остается гибким, понятным и масштабируемым:
писать тестовые сценарии стало значительно проще — не нужно держать в голове десятки названий WebSocket-сообщений;
система работает автономно и не зависит от конкретного тестового фреймворка — ее можно вынести в отдельный репозиторий и запускать независимо от окружения;
удобный дебаг избавил от множества рутинных проблем, помог быстрее находить ошибки и стал особенно полезен для новых членов команды.
А главное, всё это доступно каждому. На GitHub выложена простая реализация с готовой структурой проекта, примерами файлов, фабриками и их логикой, а также отдельными кейсами для нестандартных сценариев. Любой желающий может попробовать систему в действии и убедиться в ее эффективности.
Это идеальный старт для легкого внедрения, особенно если вы запускаете новый проект.
digtatordigtatorov
Ссылочки на гит не нашлось(