Альф, переведи мне на телефон миллион рублей
Или нюансы тестирования (и разработки) голосового помощника в банковском приложении.
В нашем приложении Альфа-Мобайл с октября 2021 работает голосовой помощник Альф (Alf). Он умеет оплачивать счета, переводить на телефон и озвучивает курс доллара голосом Геральта — Всеволода Кузнецова.
Хотя другие голосовые помощники появились раньше, о них есть и статьи и доклады, но когда мы начали тестировать нашего Альфа, было немного боязно драйвово и интересно. Мы — это Роман Кудрявцев и Максим Немченко, специалисты по тестированию в командах разработки помощника. Мы расскажем как устроен наш голосовой помощник, как мы тестируем его навыки (даже после лечения зубов) и модуль Яндекса, который закрывает слова звёздочками, какие у нас тесты и автотесты в пирамиде, что такое сенситивы и для чего используем нейросеть (спойлер: от неудобных вопросов).
Сначала немного о том, что Альф умеет и как устроен.
Из чего и как сделан Альф, и что умеет
Архитектура Альфа и взаимодействие его частей выглядит так.
Сейчас расскажем по порядку что есть что.
Все начинается с приложения Альфа-Мобайл (АМ). Оно выпускается на Android и iOS. В него вшит AimyBox SDK — набор средств разработки для взаимодействия Альфа-Мобайл и Yandex SpeechKit API.
Yandex SpeechKit — сервис от Яндекса, в котором реализованы распознавание и синтез речи. Это «уши и язык» Альфа: в Yandex SpeechKit мы отправляем голос и получаем текст, и наоборот — отправляем текст и получаем синтезированную речь.
Для связывания AimyBox SDK и Yandex SpeechKit у нас есть посредники — микросервисы Альфа-Мобайла:
Conversational Platform Gate API — служит для получения данных, связанных с подготовкой ассистента к работе.
Smart Assistant gateway — через него проходят все запросы к JAICP.
Alf Mobile API«s. Выдают информацию по продуктам пользователя.
Когда пользователь заходит в голосового помощника, через Conversational Platform Gate API Альфа-Мобайл отправляет в Яндекс запрос на получение токена. После его получения все запросы между АМ и «коробкой» будут проходить через Yandex SpeechKit.
Когда пользователь произносит команду, преобразованный текст через Smart Assistant gateway отправляется в «коробку» — набор микросервисов компании Just AI (JAICP). В JAICP наши лингвисты и прописывают все сценарии и навыки нашего помощника. Именно этот сервис распознаёт намерения пользователя и обращается к остальным банковским микросервисам для выполнения операций.
Когда мы понимаем, что хочет пользователь, JAICP через Smart Assistant gateway отправляет запрос в нужный микросервис Альфа-Мобайл за нужной информацией, например, долгу по кредиту. Затем «коробка» берет этот текст и отправляет его в YSK на синтез.
В итоге пользователь получает ответ текстом и голосом.
Что умеет ALF? Основной функционал голосового помощника — это интенты.
Интент — это намерение пользователя, цель, которая движет человеком.
Делятся на 3 вида: продуктовые, навигационные и интерактивные.
Продуктовые связаны с информацией: узнать о комиссии при переводе, когда закончится льготный период кредитки или какие условия по вкладам.
Навигационные связаны с движением. Например, клиент хочет открыть счет и говорит об этом Альфу. Помощник присылает ссылку на раздел, где открывают счета.
Интерактивные: предполагают интеграцию с банковскими сервисами. С их помощью можно оплатить мобильную связь, перевести между счетами и узнать информацию по счетам, картам, кредитам.
Пара примеров интерактивных интентов: шаблоны и информация по кредитным картам.
Шаблоны. Это первый навык, который мы запустили. Если у нас есть регулярные платежи, то мы делаем шаблон — например, на оплату «интернета». Логика шаблонов Альфа такая же — создаем шаблона, например, перевода на телефон ребенку и голосом быстро все делаем. Удобно в дороге.
Как это работает:
Информация по кредитным картам. Еще мы реализовали виджет, который отображает задолженность по кредитной карте, дату ближайшего платежа и сумму.
Виды и критерии тестирования помощника
Видов тестирования у нас четыре:
Функциональное.
Тестирование совместимости.
UI-компонентов.
Лингвистическое.
Процессы тестирования мы систематизируем: пишем тестовую документацию, покрываем автотестами, следуем Definition of Done. Поэтому на каждый из видов тестирования у нас большой список требований: что должно работать, а что нет и когда. Об этом ниже.
Функциональное тестирование интентов
Правильно было бы начать с тестирования совместимости, но мы начнем с функционального тестирования интентов, навыков, о которых только что писали.
Продуктовые. Это самые простейшие интенты, когда мы смотрим информацию по продукту. Здесь важно, чтобы речь была приятная, без долгих пауз, с правильными ударениями и чтобы вместо данных по балансу Альф не рассказал про ипотечный кредит.
Навигационные. Это интенты, которые можно описать вопросом «А покажи мне, где вот это?»
Здесь мы проверяем, что пришла нужная ссылка, и что она ведет на экран с запрашиваемым функционалом.
Интерактивные. Оплата шаблонов, перевод между счетами. Тестирование этих интентов рассмотрим чуть позже, в рамках ручного тестирования.
Сенситивы. Немного предыстории. В Альфа интегрирована нейросеть CAILA. Она поддерживает неформальный разговор с пользователем, например, у Альфа можно спросить:
— Альф, какое у тебя домашнее животное?
Он ответит:
— Жду, когда появятся комнатные слоны.
Нейросеть самообучающаяся, есть много заготовленных фраз, которые добавляются постоянно.
У нас был случай, когда Альфа хитрый пользователь спросил вопрос, на который он выдал не тот ответ, который от него ждали.
Поэтому мы начали работу над стейтами по определенным «чувствительным» ключевым словам. Сейчас их 30, они относятся к темам, которыми не должен заниматься голосовой помощник, вроде политики или религии.
Как мы тестируем сенситивы? Пытаемся поставить Альфа в неудобное положение. Если до работы над сенситивами Альф отвечал даже отчасти остроумно, но неприемлемо…
…то теперь отвечает корректнее.
Если вы нашли какие-то похожие ситуации с Альфом — пишите в комментариях.
Тестирование совместимости
Здесь мы проверяем как Альф работает с разными версиями ОС на Android и iOS, с внешними и внутренними устройствами и другими голосовыми помощниками.
Например, вот правила, которые мы соблюдаем при проверке взаимодействия помощника с аппаратным обеспечением устройства: микрофоном и динамиками.
Микрофон должен:
срабатывать автоматически, когда Альф закончил говорить;
распознавать речь;
выключаться автоматически как распозналась речь пользователя.
включаться и выключаться с помощью анимированной кнопки.
Однажды из-за того, что микрофон срабатывал заранее, включался, когда помощник говорил, получилось, что Альф общался сам с собой. Причина в том, что из-за нестабильной скорости интернета речь Альфа может прийти с задержкой. Помощник не успевал договорить, , а микрофон уже начал слушать сам себя, распознавал себя как пользователя и сам с собой общался.
Как это было:
Звуки помощника должны воспроизводиться через основной динамик, а громкость должна регулироваться. Звуки входящего звонка не должны прерывать распознавание голоса, как и уведомления или музыка.
Также при тестировании совместимости мы проверяем как работают наушники и колонки.
Наушники и колонки должны работать, как проводные, так и беспроводные.
Звуки должны воспроизводиться, если устройства подключены как до включения Альфа, так и после.
Микрофон должен работать на телефоне, а не в наушниках.
Звук голоса Альфа должен воспроизводиться через основное устройство (динамик телефона).
В блок тестирования совместимости входит и проверка взаимодействия Альфа и других голосовых помощников, потому что Siri умеет вызывать Альфа — это наша киллер-фича быстрого запуска.
Как вызвать Альфа с помощью Siri:
Сложности тестирования Siri
Siri умеет вызывать Alf. Нам без проблем удалось их интегрировать (подружить). Но с ней были небольшие сложности при тестировании. Интеграция с Siri проходит через приложение Shortcuts (Команды), и при разработке и тестировании возникли некоторые нюансы.
Приложение работает с iOS 12 и выше. Альф на iOS 11 не сможет настроить Siri для быстрого запуска.
Приложение Shortcuts можно удалить. Мы думали, что это невозможно — оно же системное. Но поняли, что ошибались, когда пользователи жаловались на то, что у них не получается настроить команду через голосового помощника из-за того, что они удалили приложение Shortcuts.
Теперь, сли удалено приложение Shortcuts, то вместе с ответом Альф пришлет ссылку на скачивание приложения в AppStore.
Поэтому мы настроили все так, что Альф будет показывать подходящий ответ, даже если шорткат создан, отредактирован и удален.
Тестирование UI-компонентов
Начинается с анимаций иконок Альфа. Когда он думает или слушает то должны появляться отдельные анимации иконок. Например, как на скрине ниже.
Быстрые навыки. На главном экране. Если тапнуть по любому навыкам, то можно задать вопрос не говоря с ним. Если нажать на вопросительный знак, то пользователь перейдет в каталог навыков и сможет посмотреть, что ещё есть у Альфа.
Текстовые баблы. В них текст, как в комиксах, иллюстрируется речь Альфа и пользователя.
Картинки SLovoDna. Мы активно сотрудничаем с этим пабликом и добавляем картинки с шутками в диалог.
Тестируя, мы проверяем все эти элементы.
Лингвистическое тестирование
Распознавание. Пользователи говорят по-разному: быстро, медленно, ровно, рвано, пропуская буквы, шумно, тихо. Но как бы пользователь ни говорил, Альф его должен понять. Поэтому при тестировании мы постоянно придумываем разные способы сказать Альфу команды.
Шумы. У вас на заднем плане может кричать ребенок, проезжать машины, шуметь люди. Это тоже может влиять. Не должно, но может.
Однажды Яндекс сделал небольшие изменения в синтезе и у нас возникла одна бага. Пользователь пожаловался, что когда сидел в пустой комнате с запущенным Альфом и ничего не говорил, слались запросы: «Слушай, Яндекс», «Привет, Алиса» и всё такое. Оказалось, что чувствительность микрофона была настроена так, что шум воспринимался как голос, звуки и Альф реагировал.
Качество речи. Когда Альф отвечает, он должен говорить без шумов, ставить ударения, делать паузы и всё это на человеческой скорости. За синтез речи (имитирование) у нас отвечает модуль Яндекса, поэтому мы их постоянно сравниваем. Например, посмотрите, как мы проверяли два варианта синтеза: версию №1 и №3.
На видео отметили как в некоторых местах помощник «заикается». Мы эти моменты отловили, зарепортили и отправили разработчикам.
Попадание в стейты или сценарии. Когда человек говорит ключевую фразу/слово, то запускается сценарий. Наша задача — отлавливать проблемы, когда фраза не попала в стейт. Например, ниже скрин ответа Альфа на фразу «беспроцентный период».
А здесь Альф не смог ответить, хотя в слове поменялась всего одна буква —«безпроцентный». Хотя причина не в слове, а в том, что Альфа сбило слово «когда»
Мы отлавливаем такие проблемы и передаем лингвистам, а они расширяют список синонимов, чтобы таких багов было меньше.
Маскировка нецензурной лексики.
Благодаря богатству русского языка Альф часто узнает много нового о себе когда чего-то не знает или не умеет. Но политика банка обязывает маскировать эти слова. Это возможно также благодаря модулю Яндекса.
И его также нужно тестировать. Поэтому мы работали над тем, чтобы выявить немаскируемые нецензурные слова. Выглядело это как взрослый человек, который 2 часа подряд матерится в телефон. В результате собрали список из 40 слов, что отправился прямиком в Яндекс.
Это на случай, если кто то сомневается в творчестве на работе:)
Теперь посмотрим на тесты.
Пирамида тестирования для голосового помощника
Тестируем ручками и автотестами. Автотестами покрываем 3 уровня: UI, API и Unit. Следуем пирамиде тестирования. Сейчас расскажем, как мы по ней двигаемся.
Unit-тесты пишут разработчики, подробно останавливаться не будем, рассмотрим автотесты, которые пишем мы — QA.
АPI-тесты
Стек: Java, Retrofit, jUnit 5 и Lombok. Сейчас API-тестов 92:
Conversational-platform-gate-api — 16 тестов.
Smart-assistant-api — 75 тестов.
Onboarding-api — 1 тест.
В рамках onboarding-api реализован один автотест. Он проверяет есть ли данные для онбординга голосового помощника.
GET mobile/api/v2/onboarding/?screen=voice_assistant
Перейдем к тестам Conversational-platform-gate-api.
Base URL: mobile/api/v1/conversational-platform-gate/
Перечень проверок, покрытых автотестами (GET-запросы, которые подготавливают помощника к работе):
GET abilities — навыки.
GET quick-abilities — быстрые навыки.
GET providers/tokens — токен.
GET bot-clients/default-configuration — конфигурации.
GET animations/{animation_name} — анимации (иконки).
GET stickers/{stickerName} — картинки SlovoDna.
Для каждой «ручки» написано по 1–2 теста. Для примера покажем как проверяем, что навыки каталога навыков главного экрана помощника работают правильно.
@Service(type = ServiceType.MICROSERVICE, path = "mobile/api/v1/conversational-platform-gate/")
public interface ConversationalPlatformGateService {
@GET("quick-abilities")
Call getQuickAbilities(
@Header(OS_KEY) String os, @Header(OS_VERSION_KEY) String osVersion, @Header(APP_VERSION_KEY) String appVersion, @Header(CHANNEL_ID_KEY) String channelID);
}
public class ConversationalPlatformGateManager{
@SneakyThrows
public QuickAbilitiesResponse getQuickAbilities(String os, String osVersion, String appVersion, String channelID) {
return conversationalPlatformGateService.getQuickAbilities(os, osVersion, appVersion, channelID)
.execute()
.body();
}
}
public class ConversationalPlatformGateTests{
private static final String MOBILE_PAYMENT = "Оплати мой мобильный";
private static final String QUERY = "QUERY";
@DisplayName("Проверка получения каталога навыков для главного экрана голосового помощника")
@ParameterizedTest(name = "{0}, osVersion: {1}")
@MethodSource("quickAbilitiesInputValues")
public void checkGetQuickAbilitiesCatalog(String os, String osVersion, String appVersion, int size, String title, String type) {
QuickAbilitiesResponse response = conversationalPlatformGateManager.getQuickAbilities(os, osVersion, appVersion, CHANNEL_ID);
int abilitiesSize = response.getAbilities().size();
AbilityElement abilities = response.getAbilities().get(abilitiesSize - 1);
AbilityElementAction action = abilities.getAction();
assertAll(
() -> assertThat(abilitiesSize, equalTo(size)),
() -> assertThat(abilities.getTitle(), equalTo(title)),
() -> assertThat(abilities.getDescription(), not(isEmptyString())),
() -> assertThat(action.getType(), equalTo(type))
);
}
private Stream quickAbilitiesInputValues() {
return Stream.of(
Arguments.of(OpSystem.ANDROID.getSystemName(),
DEFAULT_OSVERSION, DEFAULT_ANDROID_APP_VERSION, 3, MOBILE_PAYMENT, QUERY),
Arguments.of(OpSystem.IOS.getSystemName(),
DEFAULT_IOS_OSVERSION, DEFAULT_APPVERSION, 4, "Настрой команду для Siri", "SHORTCUT"),
Arguments.of(OpSystem.IOS.getSystemName(),
DEFAULT_OSVERSION, DEFAULT_APPVERSION, 3, MOBILE_PAYMENT, QUERY)
);
}
}
В тестовом методе передаем значения хедеров в классе. Отправляем запрос, получаем ответ и запускаем 2 теста: для iOS и Android.
Для iOS последней версии проверяем, что пришло 4 навыка. Последний навык по списку — настрой Siri и тип навыка — Shortcuts.
Для Android последней версии проверяем, что пришло 3 навыка. Последний навык «Оплати мой мобильный», тип — QUERY.
Для iOS 11 версии проверяем, что пришло 3 навыка. Последний навык «Оплати мой мобильный», тип — QUERY.
Для Smart Assistant gateway у нас всего одна «ручка», но метод уже не GET, а POST.
POST mobile/api/v1/smart-assistant
Поэтому мы можем передавать различные параметры и имитировать общение клиента с голосовым помощником. Автотестами покрыто 14 типов проверок. В скобках указано сколько автотестов для данного типа у нас есть:
Попадание в стартовый интент (1).
Получение ошибки (catchAll) при запросе на выполнение существующего интента (1).
Узнать баланс всех средств (1).
Получение навыка настройки команды Siri (2).
Закрыть Альфа голосом (2).
Получить предложения по кредиту (3).
Получить предложения по кредитной карте (3).
Получить ипотечное предложение (3).
Получить диплинк в навигационных интентах (8).
Запрос остатка/задолженности, ближайшего платежа по ипотеке (2).
Получить suggest (2).
Узнать остаток по счету (6).
Навыки оплаты по шаблонам (22).
Получить информацию по кредитным картам (7)
Покажем как работает проверка в Smart Assistant gateway на примере теста «Проверка остатка по счету при двух счетах с одинаковым названием». В классе @Service
укажем «ручку» с методом POST, укажем хедер и тело запроса.
@Service(type = ServiceType.MICROSERVICE, path = "mobile/api/v1/")
public interface SmartAssistantService {
@POST("smart-assistant")
Call sendIntent(@Header(OS_KEY) String os,
@Header(OS_VERSION_KEY) String osVersion,
@Header(APP_VERSION_KEY) String appVersion,
@Body MainRequestToJaicp requestBody);
}
В тестовом методе мы передаем значения хедеров, в теле запроса указываем QUERY, это будет остаток по текущему счету. Отправляем запрос, получаем ответ, для iOS и Android.
public class SmartAssistantManager {
@SneakyThrows
public MainResponseFromJaicp sendIntentWithVersions(String os, String osVersion, String appVersion,
MainRequestToJaicp requestBody) {
return smartAssistantService.sendIntent(os, osVersion, appVersion, requestBody)
.execute().body();
}
}
public class SmartAssistantHardcodedUserTests{
@DisplayName("Проверка остатка по счету при двух счетах с одинаковым названием")
@ParameterizedTest(name = "{0}")
@MethodSource("osInputValues")
public void checkBalanceOnTwoAccountsWithSameTitles(String os, String osVersion, String appVersion) {
String query = "Покажи остаток на счете текущий";
MainResponseFromJaicp response = smartAssistantManager.sendIntentWithVersions(os, osVersion, appVersion,
smartAssistantHelper.buildRequestForJaicp(query));
List responseAccounts = response.getReplies().get(1).getAccounts();
List expectedAccounts = accountApiManager.getAccounts(user).getAccounts()
.stream()
.filter(account -> account.getDescription()
.equalsIgnoreCase("Текущий счёт"))
.collect(Collectors.toList());
assertAll(
() -> assertEquals(query, response.getQuery()),
() -> assertEquals("/account/balanceRequestedByUser", response.getData().getNlpClass()),
() -> assertEquals(responseAccounts.size(), 2),
() -> assertEquals(expectedAccounts.get(0).getAmount(), responseAccounts.get(0).getAmount()),
() -> assertEquals(expectedAccounts.get(1).getAmount(), responseAccounts.get(1).getAmount())
);
}
private Stream osInputValues() {
return Stream.of(
Arguments.of(OpSystem.ANDROID.getSystemName(), DEFAULT_OSVERSION, DEFAULT_ANDROID_APP_VERSION),
Arguments.of(OpSystem.IOS.getSystemName(), DEFAULT_IOS_OSVERSION, DEFAULT_APPVERSION)
);
}
}
Получили ответ, что «коробка» (JAICP) нашла 2 счета, и мы проверяем суммы, которые прислала «коробка» с суммами от account-api
UI-автотесты
Да, мы сделали UI-автотесты для голосового помощника. Зачем?
Мы выпускаем приложение с 5 по 12 Android и с 11 по 15 iOS. При этом в отдельных версиях приложений у нас были краши., например, в 8 Android и 13 iOS.
Этими автотестами мы проверяем базовые функции, что голосовой помощник запускается, приложение не падает, на экране есть базовые UI-элементы, появляются UI-компоненты, о которых говорили выше.
Эти тесты запускаем на регрессе, проверяем приложение на версиях ОС с 5 по 12 Android и с 11 по 15 iOS.
Автотесты позволяют нам выявить проблемы на ранней стадии, что экономит нам (и нашим разработчикам) много времени и нервов.
Пример теста. Стек: Java 11, Appium, jUnit 5 и Lombok.
@Component
@Name("Главный экран ГП")
public class VoiceAssistantMainPage extends Page {
@Required
@AndroidFindBy(accessibility = "Назад")
@iOSXCUITFindBy(accessibility = "Закрыть")
private MobileElement backButton;
@Required
@AndroidFindBy(xpath = "//android.widget.TextView[@text = 'Голосовой помощник Alf']")
@iOSXCUITFindBy(iOSNsPredicate = "label == 'Голосовой помощник Alf'")
private MobileElement vaHeader;
@Required
@AndroidFindBy(xpath = "//android.widget.TextView[@text = 'Сколько у меня денег?']")
@iOSXCUITFindBy(accessibility = "Сколько у меня денег?")
private MobileElement balanceQuickAbility;
@Step("Быстрый запрос баланса")
public void quickBalance() {
balanceQuickAbility.click();
}
@Step("Ответ на запрос баланса")
public boolean checkBalanceResponse() {
return ElementActions.isElementOnScreen(balanceResponse)
&& ElementActions.isElementOnScreen(balanceComponent);
}
Пример теста.
@Test
@DisplayName("Проверка базовых функций Голосового помощника")
@Tags({@Tag("ANDROID"), @Tag("IOS")})
public void checkVABase() {
fastRegistration(user.getLogin());
mainPage.openVoiceAssistant();
voiceAssistantMainPage.preloadActions();
voiceAssistantMainPage.loadPage();
voiceAssistantMainPage.quickBalance();
assertTrue("Баланс не найден", voiceAssistantMainPage.checkBalanceResponse());
voiceAssistantMainPage.openAbilitiesPage();
voiceAssistantAbilitiesPage.loadPage();
voiceAssistantAbilitiesPage.balanceClick();
assertTrue("Ответ ГП на навык баланса не найден", voiceAssistantMainPage.checkBalanceResponse());
voiceAssistantMainPage.closeVA();
В этих тестах нет проверки голоса для голосового помощника. Сейчас у нас нет готового решения для передачи звуковых дорожек в тест. Активно изучаем этот вопрос.
Если есть советы, рекомендации или вы знаете как нам решить эту проблему — пишите в комментариях, будем благодарны!
Ручное тестирование
Завершает пирамиду. Для ручного тестирования у нас заведены тест-кейсы. Сейчас у нас их 52.Этого количества нам хватает на все наши фичи. Простейший выглядят примерно так.
Здесь есть описание, цель, условия и шаги: что делать, чтобы провести тест, и что должно появиться/произойти в качестве результата.
Заключение
На тестирование голосовых помощников действуют те же правила, что и на все остальное: сайты, приложения, веб-сервисы.
Автотесты для голосового помощника? Почему бы нет.
Тестировать голосовой помощник интересно, потому что возникают нестандартные задачи, например, тесты на бегу, в лифте или у стоматолога. Тренирует голову, скилл упёртости и фантазии, потому что надо учитывать особенности помощников, которых нет у приложений.
В целом, всё. Если у вас есть вопросы или дополнения к статье — пишите в комментариях, или нам напрямую: @eilinwis и @maxon2112. Заходите в чат в Телеграм, где мы общаемся на тему тестирования, там тоже обсудим статью.
Если хотите поработать над голосовым помощником — приходите, сейчас как раз нужен Продуктовый лингвист в команду Голосового помощника Alf. Также ждём QA Engineer C# и QA Lead C#, но на другое направление: .
Рекомендуем почитать: