Автоматизация тестирования голосового чат-бота в Google DialogFlow на Python

от автора

С чего всё началось?

Ещё на этапе собеседования в текущую команду передо мной поставили вопрос:

Применима ли автоматизация тестирования при проверке функционала чат‑ботов?

Не решав подобные задачи ни разу, я с полной уверенностью сказал «Да!», и начал изучать текущее решение. Собственно:

Текущее решение

В компании есть текстовые чат-боты (Telegram, WhatsApp, Viber) и голосовой бот-помощник, с которым сталкивается большинство людей, когда-либо звонивших на номер 8 (800) и прочие централизованные номера корпораций.

Процессы в текстовых чат-ботах были налажены — бот отвечал на вопросы по пунктам, либо переводил диалог на оператора. Но вот в приложении голосового бота всё было гораздо интереснее:

  1. Озвучивание нашего текста

  2. Транскрибация запроса клиента

  3. Сопоставление запроса с интентами в DialogFlow

  4. Получение информации с бэка

Количество интентов велико, правки и дополнения вносятся часто, вследствие чего было принято решение подключать автоматизацию именно здесь.

Способы тестирования

На самом деле, варианта всего два: тестирование с использованием голосовых команд, либо тестирование интентов напрямую в DialogFlow, но дьявол кроется в деталях. Разберем эти пункты отдельно.

Тестирование голосом

MicSwap или Virtual Audio Cable — приложения на Windows для эмуляции микрофона, с помощью которых (чисто теоретически) можно проигрывать голосовые дорожки на виртуальный микрофон при тестировании голосового бота через, например, софтфон. Вариант был отложен «на подумать» по следующим причинам:

  1. Необходим сервис генерации фраз клиентов

  2. При использовании статичного набора фраз нужно место для их хранения

  3. Необходимы дополнительные проверки правильности распознавания и транскрибации голоса, предварительное решение — параллельный мониторинг логов, либо локальное распознавание.

Тестирование через DialogFlow

В части ручного тестирования DialogFlow предусмотрел всё, что вам нужно. Всё, в данном случае — это симулятор в консоли, который позволяет вести диалог и сразу видеть всю необходимую информацию — реакцию, метаданные, интент, контексты.

Попробуй, я настаиваю. (с)

Попробуй, я настаиваю. (с)

Вся магия при ручном тестировании происходит именно здесь — агенты для прода, стейджа, тестового стенда, и консоль для проверок свежих правок по нашим диалогам.

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

Автотесты v1.0 (UI)

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

@allure.step("Написать: {phrase}") def write(self, phrase:str):     """Отправка сообщения в текстовое поле - эмуляция фразы клиента"""     input = self.driver.find_element(By.CSS_SELECTOR, '#test-client-query-input')     input.send_keys(phrase)     input.send_keys(Keys.ENTER)  @allure.step("Проверить текст ответа") def checkResponse(self, text:str):     """Проверить текст ответа на реплику от DF."""     response = self.driver.find_element(By.CSS_SELECTOR, 'div[title="Text Response"] span').get_attribute('innerHTML') #span     assert text in response, 'Бот ответил:\n' + response + '\nБот должен был ответить:\n' + text

Отдельно стоит упомянуть ситуацию, когда текст отображается в <span> — контейнере: для его экстракции нужно использовать метод get_attribute('innerHTML').

Первый тест готов, диалоги пишутся напрямую в скрипте. Параметры клиента задаются в нём же. При написании второго теста начинают чаще одолевать мысли «как бы вынести все данные для теста из самого скрипта, чтобы уменьшить количество кода и его дублей, и чтобы было удобно? Решение красиво и банально — весь диалог вынести в отдельный текстовый файл…

!2 Здраствуйте, я голосовой помощник! Cкажите что вас интересует. сколько осталось топлива Правильно ли я поняла, что вам нужно узнать остаток бензина в баке? да Остаток топлива в баке 15 литров.

…дописать метод для работы с этим файлом…

def executeTest(self,                 allure_header,                 path_to_phrases_file="tests/regress/dialogs/dialog.txt"):         @allure.step(allure_header)         def executeTestWithHeader():             with open(path_to_phrases_file, encoding="utf-8") as f:                 phrases = f.readlines()                 f.close()             self.DF_login()             clientIndex = 0             botIndex = 1             while clientIndex < phrases.__len__():                 self.write(phrases[clientIndex][:-1])                 self.checkResponse(phrases[botIndex][:-1])                 clientIndex = clientIndex + 2                 botIndex = botIndex + 2         executeTestWithHeader()

…указав индексы для фраз клиента и бота, не забывая передавать строки, полученные из файла со срезом последнего символа (\n), и вуаля — наш тест, состоящий из одного метода, для выполнения которого нужен всего лишь заголовок теста для отчёта в Allure и диалог в текстовом файле, готов.

Автотесты v2.0 (API)

Мы меняем версии как перчатки только в статье, поэтому не стоит пугаться столь мажорных числовых перемен. Вот почему мы выполняем условный инкремент.

Тесты прекрасно работают локально, с боевыми данными один кейс проходит за ~90 секунд и для небольшого набора тестов это приемлемый результат. Главная проблема выяснилась на этапе построения пайплайна в GitLab — тесты упорно не хотели там проходить. Причина была проста до безобразия: политика безопасности Google для аккаунтов.

В связи с большой удалённостью (физической и сетевой) друг от друга инфраструктуры и моего рабочего места, учетной записи требовалась двухфакторная авторизация — подтверждение телефона, проверочный код, выбор чисел на экране телефона и тому подобное. Казалось бы, можно было бы повысить степень доверия, проставив дополнительные ожидания и успеть прокликать всю верификацию, но коллеги из инфры дали ценный совет:

Переделывай на API. Такие тесты всегда проще и короче.

Предвкушая освоение новой области, были начаты поиски подходящих вариантов и внезапно выяснилось, что у DialogFlow и вправду есть API.

Используя ещё одну прекрасную, но заблокированную большим братом инструкцию настраиваем наши тесты. Пишем инициализацию класса, в котором задаем переменную с ключом доступа к API и создаем сессию.

from google.cloud import dialogflow_v2beta1 as dialogflow import os import random  class Events:      def __init__(self):         os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = 'API/credentials/voicebot.json'         self.input = dialogflow.TextInput()         self.input.language_code = 'Russian-ru'         self.DIALOGFLOW_PROJECT_ID = 'voicebot'         self.DIALOGFLOW_LANGUAGE_CODE = 'Russian — ru'         self.SESSION_ID = 'autotest_' + str(random.randint(1,999999))         self.session_client = dialogflow.SessionsClient()         self.session = self.session_client.session_path(self.DIALOGFLOW_PROJECT_ID, self.SESSION_ID)

Переписываем методы для работы с сообщениями и добавляем проверку интента:

@allure.step("Написать: {phrase}") def write(self, phrase:str):     """Эмуляция фразы клиента"""     self.phrase = phrase     print(self.phrase)     self.text_input = dialogflow.TextInput(text=self.phrase, language_code=self.DIALOGFLOW_LANGUAGE_CODE)     self.query_input = dialogflow.QueryInput(text=self.text_input)  @allure.step("Проверить текст ответа бота и интент") def checkResponse(self, text:str, intent:str):     """Проверить текст ответа бота и интент"""     self.response = self.session_client.detect_intent(session=self.session, query_input=self.query_input).query_result     self.response_text = str(self.response.fulfillment_messages)     self.response_intent = str(self.response.intent.display_name)     index1 = self.response_text.find('"')     self.response_text = self.response_text[index1+1:]     index2 = self.response_text.find('"')     self.response_text = self.response_text[:index2]     assert text in self.response_text, '\nБот ответил: ' + self.response_text + '\nБот должен был ответить: ' + text     assert intent in self.response_intent, '\nКлиент сказал: ' + self.phrase + '\nОтвет бота: ' + self.response_text + '\nИнтент диалога в DF: ' + self.response_intent + '\nОжидался интент: ' + intent  def executeTest(self,                 allure_header:str,                 intent:str="dialog"):     @allure.step(allure_header)     def executeTestWithHeader():         dialog_path = 'tests/regress/dialogs/' + intent + '.txt'         intent_path = 'tests/regress/intents/' + intent + '.txt'         with open(dialog_path, encoding="utf-8") as f:             phrases = f.readlines()             f.close()         with open(intent_path, encoding="utf-8") as i:             intents = i.readlines()             i.close()         clientIndex = 0         botIndex = 1         intentIndex = 0         while clientIndex < phrases.__len__():             self.write(phrases[clientIndex][:-1])             self.checkResponse(phrases[botIndex][:-1], intents[intentIndex][:-1])             clientIndex = clientIndex + 2             botIndex = botIndex + 2             intentIndex = intentIndex + 1     executeTestWithHeader()

По аналогии с файлом диалога добавляем файл с построчным списком интентов, по которым мы планируем проходить:

ivr - begin ivr - how_much_fuel how_much_fuel - response

Пишем простейший тест:

from helpers.Events import Events  def test_fuel():      e = Events()     e.executeTest("Проверка остатка топлива"                   ,"dialog")

И получаем наш профит:

  1. Не используем UI там, где его можно не использовать

  2. Получаем тесты с авторизацией через токен приложения — запускаем где угодно

  3. Уменьшаем количество кода и делаем его более лаконичным

  4. Сокращаем время прогона с ~90с до ~10c на тест

Заключение

На данный момент тесты используются для проверки малого регрессионного набора, основное же их назначение ввиду отсутствия большого регресса — проверка новых интентов, поскольку сейчас идет большая переделка всей структуры бота для разбиения областей взаимодействия с клиентом. Как раз в связи с этим было принято решение сделать написание новых тестов максимально простым — нужно добавить файл скрипта Python, два текстовых файла с диалогом и списком интентов, после чего тест уже можно использовать для проверок.

Вступаю в ряды любителей тестов через API, призываю вас поступить так же и желаю успехов в освоении новых областей знаний!


ссылка на оригинал статьи https://habr.com/ru/articles/762982/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *