При тестировании распределенных систем разработчики сталкиваются с асинхронным взаимодействием с серверами, громоздкими сценариями отправки и сложным входом для новичков. Это приводит к ошибкам, долгой отладке и росту затрат.

Привет, Хабр! Меня зовут Максим Попов, я инженер по автоматизированному тестированию внутренних продуктов в Сбере — в том числе 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 — при открытии соединения клиент сразу шлет все сообщения, а ответы принимает без разбора. 

Но такая реализация не решает ни одной из наших задач. Кроме того, у нее есть другие критичные недостатки:

  1. Нет ясности, что отправлять. Чтобы перевести систему в состояние B, нужно знать, какие сообщения отправить. Это может быть огромная сложная цепочка сообщений, с которой, может быть лень разбираться Стандартный клиент ее не составит, поэтому приходится разбираться вручную.

  2. Нет контроля ожиданий. WebSocket — асинхронный протокол. Иногда нужно приостановить отправку и дождаться данных, вроде interactionID. Примитивный клиент это не умеет — он шлет все подряд и не реагирует на условия.

  3. Нет обработки сообщений. Непонятно, как обрабатывать огромную разновидность сообщений на вход и на выход.

Поэтому мы остановились на собственной реализации, которая не имеет особых требований — все реализуется на Python через стандартную библиотеку asyncio. Также можно использовать gevent и запускать задачи чтения и отправки в greenlet-потоках.

Архитектура фреймворка и её проблемы

Теперь об архитектуре нашего подхода, и сразу — о типовых проблемах, с которыми можно столкнуться при реализации асинхронных WebSocket-клиентов. 

Проблема №1: Вариативность сценариев

Тестовые сценарии в SCPL бывают сложными и непредсказуемыми. Вот один из простых, но реальных примеров работы:

  • оператор меняет статус на «Готов»;

  • затем на «Не готов»;

  • снова на «Готов»;

  • делает вызов;

  • завершает звонок;

  • снова отправляет сообщение.

Каждое бизнес-действие может скрывать за собой сколько угодно WebSocket-сообщений — здесь легко запутаться, особенно если сценариев десятки.

Решение: 

Упростить сценарии, сделать их понятными в описании в коде и повторяемыми в тестировании. 

Проблема №2: Сложный порог входа для новичков

Когда стажеру нужно написать тест с отправкой 20 WebSocket-сообщений в правильном порядке — задача выглядит пугающе. Документация не помогает — в ней описаны только типы сообщений, а не логика их применения.

Решение:

Чтобы облегчить процесс, делим сообщения на два уровня:

  • сценарные — описывают логику теста верхнеуровнево.

  • технические — то, что реально отправляется на WebSocket.

Теперь вместо ручного создания цепочки NewOutInteractionStartNewOutInteractionAcceptRTC используется одно сценарное сообщение — 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_statusoutbound_callagent_hangup.

  • гарантия порядка. WebSocket-сообщения всегда отправляются в правильной последовательности.

Проблема №3: Регулярное появление нового функционала

Платформа развивается постоянно. Появляются:

  • новые сообщения (MessageA, MessageB);

  • новые ответы от сервера (ResultA, ResultB);

  • новые логики обработки и зависимости между ними.

Когда все прописано вручную, код превращается в хаос из методов и if-блоков. Поэтому нужна система, которая автоматически обрабатывает новые элементы и структурировано хранит все данные.

Решение:

Для отправляемых и получаемых сообщений применили паттерн «фабрика». Вот как это работает:

  • по имени сообщения выбирается соответствующий обработчик;

  • он сам решает, какой алгоритм применить;

  • фабрики обрабатывают сообщения серверу и от него

В итоге получается единообразная система, которую легко расширять. Например, если добавилось новое сообщение — его достаточно зарегистрировать в фабрике.

Теперь объясню, почему мы выбрали именно фабрику:

  1. Инкапсуляция логики. Для каждого сообщения в фабрике указываются правила обработки, алгоритм выполнения и поля для извлечения/подстановки. Если сообщение можно обработать уже существующим алгоритмом, новый код писать не нужно — просто указываем правило повторного использования.

  2. Гибкость. Фабрики разделяются по назначению — одна управляет отправкой, другая — приемом. При этом все обработчики лежат централизованно и в коде легко ориентироваться. 

А добавление новой логики сводится к простым шагам — создаём константу сценарного сообщения, привязываем её к нужному обработчику и регистрируем в фабрике.

Вот как на практике выглядит отправка сообщений:

  • асинхронная задача вызывает фабрику и передает туда сообщение;

  • фабрика по имени сообщения выбирает обработчик из хеш-таблицы;

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

Это идеальный старт для легкого внедрения, особенно если вы запускаете новый проект.

Комментарии (0)


  1. digtatordigtatorov
    19.09.2025 13:41

    Ссылочки на гит не нашлось(