Simply Business — ведущий брокер бизнес-страхования в Великобритании, защищающий более 300 000 компаний и арендодателей по всей стране. Когда клиенты звонят в компанию, они хотят быть уверены в наилучшем обслуживании — без долгого ожидания на линии и необходимости повторять одну и ту же информацию снова и снова. Чтобы улучшить клиентский опыт, компания модернизировала свой колл-центр с помощью Twilio TaskRouter. В этой статье, первоначально опубликованной в блоге Simply Business, рассказывается об этом процессе.
Лукас Оберхубер, технический директор Simply Business, расскажет о том, как компания масштабировала свой колл-центр для достижения успеха на конференции SIGNAL London 20 сентября. Зарегистрируйтесь здесь, чтобы получить билеты.
В Simply Business мы в настоящее время работаем над проектом по замене существующего колл-центра на Twilio TaskRouter и Voice API.
Этот проект сильно отличается от наших обычных страховых приложений, и мы столкнулись с рядом проблем, с которыми обычно не сталкиваемся, в основном связанных с настройкой непрерывной интеграции и тестирования. Мы хотели бы поделиться своим опытом.
Мы не пришли к окончательному решению за один день. Мы начали с автоматизации простого сценария пользовательского пути, затем постепенно улучшали процесс до полной автоматизации на платформе Semaphore CI — внешнем сервисе непрерывной интеграции (CI).
Чтобы дать представление о том, как работает наше приложение, ниже представлена модифицированная схема взаимодействия фронтенда, бэкенда и Twilio с конечными пользователями и консультантами колл-центра (которые выступают в роли Worker в терминологии TaskRouter от Twilio).
— Наше приложение отправляет задачу в Twilio.
— Twilio вызывает наш бэкенд через /task_assigned.
— После подтверждения Twilio отправляет WebSocket-событие reservation.created на фронтенд.
— Получив событие на фронтенде, мы вызываем Worker.
— Worker принимает звонок через фронтенд-софтфон (WebRTC, инкапсулированный объектом Twilio.Device JS).
— Twilio вызывает наш бэкенд через /worker_joined и подключает к конференции.
— После подключения Worker бэкенд звонит на реальный телефон пользователя.
— Пользователь отвечает на звонок.
— Twilio вызывает наш бэкенд (/user_joined) и подключает к конференции.
— Для простого бэкенд-сервиса мы используем sinatra.rb, а для создания одностраничного приложения (SPA) — React.js.
Прежде чем пользователь и консультант присоединятся к одной конференции для начала разговора, необходимо отправить множество сообщений между нашим приложением и Twilio. Как видно, основная логика нашего приложения тесно интегрирована с окружением Twilio.
Мы хотели иметь набор интеграционных тестов, которые давали бы уверенность в том, что наш код корректно интегрируется с Twilio при каждом развертывании в продакшн.
В течение трех месяцев мы постепенно улучшали конфигурацию, устраняя повторяющиеся задачи. Однажды наш инженер Питер сказал: «На самом деле, мы могли бы просто запустить этот тест в CI-окружении». Он потратил пару часов на эту задачу, и всё заработало!
Далее мы рассмотрим, как мы пришли к этому результату в три этапа:
1. Прыжок (Hop)
2. Шаг (Step)
3. Прыжок (Jump).
**Прыжок** — это ваш первый шаг к написанию локальных интеграционных тестов.
Создайте отдельное окружение для интеграционного теста, который обращается к стороннему эндпоинту.
Как разработчик, вы хотите иметь возможность выполнять сквозное тестирование без моков, чтобы быть уверенным, что система работает так, как вы ожидаете.
Наш тест избегает использования моков и фактически обращается к стороннему эндпоинту Twilio. Наш spec_helper запускает полный сквозной тест, если он выполняется с тегом #integration.
Вот несколько моментов, на которые стоит обратить внимание в этом фрагменте кода:
— config.filter_run_excluding integration: true исключает тег #integration из обычного вызова RSpec, чтобы он не запускался в CI-окружении.
— WebMock.disable_net_connect по умолчанию отключает внешние соединения.
— Мы включаем MyApp.fake_connection! для имитации поведения Twilio Ruby gem (в качестве альтернативы можно использовать VCR, который записывает HTTP-запросы и ответы).
Для тега #integration:
— Мы запускаем WebMock.allow_net_connect!, чтобы разрешить тестам обращаться к внешним сервисам.
— Меняем Capybara.javascript_driver на :selenium_chrome, чтобы тесты запускались в реальном браузере, а не в безголосом по умолчанию.
Теперь, когда у нас настроено базовое окружение для интеграции, перейдем к написанию простого сценария, который позволяет Worker получить новую задачу, позвонить пользователю, а затем завершить звонок, чтобы быть готовым принять следующий.
Мы используем Turnip, который позволяет писать тесты в формате Gherkin и запускать их в окружении RSpec.
Любые методы в MyApp — это серверные REST API-вызовы к серверу Twilio, которые позволяют нам убедиться в определенном состоянии TaskRouter. MyApp.clear_tasks очищает все существующие задачи, а MyApp.update_worker_activity гарантирует, что Worker находится в определенном состоянии активности.
Любые действия в Twilio TaskRouter можно легко проверить с помощью Twilio REST Ruby gem, но мы не делали этого, так как проверка состояния активности Twilio не отражает то, что видят пользователи.
Вместо этого мы проверяем поведение фронтенда двумя способами.
Это один из типичных приемов для проверки событий на стороне клиента путем назначения QA-специфичного ID в DOM.
Как только ID задан, можно использовать обычный матчер, чтобы дождаться появления элемента «Gone Home» в интересующем вас DOM (и Capybara достаточно умен, чтобы дождаться появления элемента).
Однако как узнать, что ваша гарнитура действительно подключена через WebRTC? Если она отключена, это может быть неочевидно.
Для спокойствия мы проверяем Twilio.Device.status, чтобы получить внутреннее состояние JS-объекта.
В отличие от первого подхода, этот метод не ждет появления нужного статуса. Поэтому вам нужно написать логику для циклической проверки до тех пор, пока статус не изменится на ожидаемый.
**Получение обратного вызова о создании задачи от Twilio**
Мы почти завершили автоматизацию нашего интеграционного теста. Прежде чем проверять все события, необходимо отправить задачу в Twilio.
Вот как мы создаем новую задачу:
Вся наша инфраструктура состоит из нескольких приложений, и мы реализовали событийную архитектуру для отправки событий из одной системы в другую с помощью RabbitMQ. RabbitFeed — это Ruby gem, разработанный нашим коллегой Джошуа Флеком, который предоставляет удобный DSL для определения потребителей и издателей событий, хотя его использование выходит за рамки этой статьи (подробнее можно узнать здесь и здесь).
После публикации события наше приложение его потребляет и вызывает MyApp.create_task для создания задачи.
После создания новой задачи необходимо настроить Twilio так, чтобы он мог вызывать вашу локальную машину. Вы можете использовать SSL-туннель через существующий AWS-инстанс (в нашей среде мы можем легко создавать интеграционные инстансы для каждого разработчика) или ngrok — бесплатный сервис для предоставления динамически назначаемого публичного URL (например, akvk1c.ngrok.com).
Изначально мы выбрали SSL-туннель, так как он предоставлял статический URL, но позже перешли на ngrok, потому что хотели иметь выделенный URL-эндпоинт не только для каждого разработчика, но и для разных окружений (таких как dev, test и integration).
Еще один момент, на который стоит обратить внимание в нашей настройке, — это то, что у нас есть субаккаунты не только для каждого окружения, но и для каждого разработчика. Если в вашей команде 6 разработчиков, вам понадобится всего 21 субаккаунт:
— (dev, test, integration) * количество разработчиков = 3 окружения * 6 инженеров = 18
— integration, staging, production для системного аккаунта = 3
Благодаря этим изменениям наши тесты с тегом #integration запускают браузер и выполняют реальное сквозное тестирование с Twilio.
Недостатком является то, что вам всё еще нужно участвовать в тестовом наборе для выполнения определенных задач, таких как:
— Нажатие кнопки «Разрешить» для использования микрофона.
— Ответ на звонки каждый раз, когда назначается новая задача.
— Изменение URL обратного вызова TaskRouter каждый раз, когда ngrok назначает новый URL (что происходит нечасто).
Наш второй шаг (Step!!) был направлен на устранение этих неудобств: (1) use-fake-ui-for-media-stream и (2) use-fake-device-for-media-stream.
Первый, use-fake-ui-for-media-stream, относительно легко устранить. Вернемся к нашему spec_helper.rb и немного изменим настройки драйвера.
Эти дополнительные настройки будут имитировать подключение к микрофону браузера — больше не будет появляться всплывающее окно с запросом разрешения на доступ к микрофону (подробнее здесь). Как дополнительный бонус, эта опция позволяет запускать тест на машине без звуковой карты и микрофона. Это еще один шаг к переносу этих тестов во внешнее CI-окружение.
Хотя проблема (1) возникает только один раз за запуск тестового набора, проблема (2) повторялась несколько раз за один тестовый прогон, и их количество росло по мере увеличения количества интеграционных сценариев.
— Сценарий: Worker завершает звонок
— Сценарий: Пользователь завершает звонок
— Сценарий: Звонок прерывается по тайм-ауту
— Сценарий: Звонок не удается из-за занятости
— Сценарий: Звонок не удается из-за немедленного завершения
Проблема заключалась не только в необходимости отвечать на множество звонков, но и в том, что нужно было внимательно следить за логами тестового раннера, чтобы понимать, в каком сценарии вы находитесь, и действовать соответственно (например, завершать звонок с реального телефона для сценария завершения звонка пользователем, в то время как для сценария завершения звонка Worker нужно было дождаться завершения теста).
Ситуация усугублялась тем, что у нас был QA-инженер, работавший удаленно, и для него было слишком затратно отвечать на тестовые звонки. Поэтому каждый раз, когда он запускал тест, звонил один из наших лондонских инженеров. Нетрудно представить, насколько это могло быть разрушительно.
Здесь пригодился TwiML от Twilio.
Как вы знаете, Twilio предоставляет собственный язык разметки для интерактивного голосового ответа (IVR). В приведенном выше примере система ждет 10 секунд, произносит фразу, а затем завершает звонок.
Вы можете создать несколько TwiML-эндпоинтов для каждого сценария, разместить XML где-либо (или использовать сторонний TwiML-бин, например TwiMLbin), а затем зарегистрировать каждый URL для входящих номеров Twilio.
Когда мы опробовали этот подход, нас изначально беспокоило, что телефонные линии могут быть заняты, если два теста одновременно позвонят на один и тот же номер. Однако этого не произошло. Это была хорошая новость, так как у нас было 21 субаккаунт, и мы не хотели настраивать несколько входящих номеров для каждого из них. Вы можете настроить это один раз на своем основном аккаунте, и всё!
Решение проблем (1) и (2) позволило нам запускать автоматизированные тесты практически «без рук», за исключением случаев, когда меняется URL ngrok (например, при перезапуске ngrok).
Хорошая новость заключается в том, что у ngrok есть API-эндпоинты (по умолчанию на localhost:4040), и вы можете программно получать актуальный URL ngrok.
Поэтому мы создали следующий Ruby-модуль, который запускает ngrok (на порту 3000 или 3001), запрашивает его URL через API-эндпоинт (4040 или 4041), а затем подставляет этот URL в качестве эндпоинта TaskRouter. Это вызывается только при запуске нашего приложения с помощью foreman.
Вы можете запустить ngrok для каждого окружения с помощью NgrokRunner.start_for(@env).
После запуска вы можете получить URL ngrok с помощью NgrokRunner.url(@env).
Возможно, вам интересно, что такое окружение test_integration в исходном коде.
Это специальное окружение, которое мы подключаем только при запуске теста с тегом #integration.
Вот как вы настраиваете это в spec_helper.rb.
Благодаря нашему скрипту для динамического определения URL ngrok мы устранили главное препятствие для запуска тестов обратного вызова в стороннем CI-окружении, таком как Semaphore CI, где у нас ограниченные возможности для настройки их окружения.
Что еще осталось? На самом деле, не так много. Изначально мы думали, что установка исполняемого файла Chrome на Semaphore будет сложной задачей, но оказалось, что он уже установлен, согласно их сайту.
Таким образом, осталось установить только ngrok и драйвер Chrome. Их можно установить с помощью npm. Мы создали файл script/ci и настроили Semaphore на его выполнение в рамках теста.
Скрипт /script/ci устанавливает драйверы и запускает полный тест, если ветка — master.
Мы могли бы запускать этот тестовый набор для каждой ветки, но каждый звонок в Twilio стоит денег, поэтому мы решили запускать его только для ветки master.
В этой заключительной статье мы обсудили следующее:
— Изменение драйвера Chrome для имитации настроек микрофона.
— Настройку TwiML для приема тестовых звонков.
— Динамическое подключение URL обратного вызова ngrok.
— Запуск интеграционных тестов на Semaphore.
Эти тестовые наборы не только экономят время на ручное тестирование, но и позволяют нам уверенно рефакторить внутренний код.
На самом деле, у нас был один большой внутренний рефакторинг, в ходе которого мы изменили порядок звонков (изначально мы звонили пользователям до того, как консультант брал трубку, что приводило к тишине, когда пользователи отвечали на звонок), и без сквозного тестирования это было бы пугающе делать.