Pull to refresh

Функциональное тестирование навыков Яндекс Алисы на Node.js

Reading time 5 min
Views 5.8K

Алиса, запусти навык


Прошел почти год с того момента, как появилась возможность создавать свои навыки для Алисы — голосового помощника от Яндекса. В каталог ежедневно прибывают новые навыки, а их общее число составляет несколько сотен. К сожалению, общение с некоторыми навыками мягко говоря "не складывается". Навык или зацикливается на одной и той же фразе или вообще сломан и не отвечает.


В этой статье я рассмотрю написание функциональных автоматизированных тестов для навыка на Node.js. Наличие таких тестов позволяет создавать более качественные навыки и дает уверенность в их работоспособности.


Существующие инструменты тестирования


Навык для Алисы — это веб-сервер, который умеет отвечать на POST запросы в определенном формате. На данный момент существует несколько инструментов, куда можно передать URL навыка и проверить его работу:



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


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


Готовой библиотеки под Node.js для такой задачи я не нашел, поэтому напишем свое :)



Возьмем официальный пример навыка из репозитория Яндекса на GitHub. Это навык "Попугай", который просто повторяет все, что сказал пользователь. Построен на базе фреймворка micro и содержит всего несколько строчек кода:


// server.js
const micro = require('micro');
const {json} = micro;

module.exports = micro(async req => {
  const {request, session, version} = await json(req);
  return {
    version,
    session,
    response: {
      text: request.original_utterance || 'Hello!',
      end_session: false,
    },
  };
});

При первом заходе навык получит от пользователя пустое сообщение (original_utterance) и ответит "Hello!". В остальных случаях просто скопирует сообщение пользователя в поле response.text.


Я обернул оригинальный код примера с GitHub в функцию micro(), чтобы экспорт возвращал http-сервер, который мы и будем использовать в тестах.


Тест-план


Итак, чтобы покрыть тестами такой навык, необходимо следующее:


  1. Поднять сервер с навыком на локальном порту
  2. Проверить два кейса:
    • Пользователь заходит в навык, навык должен ответить "Hello!"
    • Пользователь отправляет сообщение в навык, навык должен ответить тем же сообщением
  3. Остановить сервер с навыком и показать отчет

Автоматизировав эти проверки, можно будет запускать их перед каждым коммитом и быть уверенным, что ничего не сломалось.


Напишем код теста согласно плану, используя синтаксис для mocha. Допустим, что у нас уже есть некоторый класс User, который умеет делать все что нужно:


// test.js
const assert = require('assert');

before(done => {
  // запускаем сервер навыка
  server.listen(PORT, done);
});

it('should get hello on enter', async () => {
  // создаем пользователя для навыка
  const user = new User(`http://localhost:${PORT}`);
  // заходим в навык и сохраняем ответ
  const response = await user.enter();
  // проверяем текст ответа
  assert.equal(response.text, 'Hello!');
});

after(done => {
  // останавливаем сервер
  server.close(done);
});

Осталось написать класс User и можно будет запускать тест.


Виртуальный пользователь


Главное, что должен уметь тестовый пользователь — отправлять POST запросы на урл навыка с данными в нужном формате. Формат запроса описан в документации. Сейчас нам не нужны все поля, поэтому я оставил только необходимые, чтобы не раздувать код примера. Класс User с комментариями:


// user.js
const fetch = require('node-fetch');

module.exports = class User {
  /**
   * Конструктор
   * @param {String} webhookUrl
   */
  constructor(webhookUrl) {
    this._webhookUrl = webhookUrl;
  }

  /**
   * Вход пользователя в навык
   */
  async enter() {
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    };
    // при заходе в навык, сообщение - пустая строка
    const body = this._buildRequest('');
    const response = await fetch(this._webhookUrl, {
      method: 'post',
      headers,
      body: JSON.stringify(body),
    });
    const json = await response.json();
    return json.response;
  }

  /**
   * Сборка тела запроса с заданным сообщением пользователя
   * @param {String} message
   */
  _buildRequest(message) {
    return {
      request: {
        command: message,
        original_utterance: message,
        type: 'SimpleUtterance',
      },
      session: {
        new: true,
        user_id: 'user-1',
        session_id: 'session-1'
      },
      version: '1.0'
    }
  }
};

Запуск


Для запуска осталось импортировать классы пользователя и сервера в файл с тестами, а также выставить значение порта, на котором поднимется сервер:


// test.js
...
const server = require('./server');
const User = require('./user');

const PORT = 3456;
...

Устанавливаем все необходимые зависимости:


npm install micro node-fetch mocha 

И запускаем тест:


$ mocha test.js

  ✓ should get hello on enter

  1 passing (34ms)

Все хорошо, тест пройден!


Но прежде чем идти дальше, нужно убедиться, что тест действительно работает. Для этого заменим в ответе навыка "Hello!" на "Привет!" и запустим еще раз:


$ mocha test.js

  0 passing (487ms)
  1 failing

  1) should get hello on enter:

      AssertionError [ERR_ASSERTION]: 'Привет!' == 'Hello!'
      + expected - actual

      -Привет!
      +Hello!

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


Учим пользователя общаться


Остался второй кейс, когда пользователь отправляет в навык сообщение и должен получить это же сообщение обратно. Чтобы пользователь смог "общаться", я добавил в класс User метод say(message). Также сделал небольшой рефакторинг: вынес отправку http-запросов в отдельный метод и использовал его внутри enter() и say(message):


// user.js
const fetch = require('node-fetch');

module.exports = class User {
  /**
   * Конструктор
   * @param {String} webhookUrl
   */
  constructor(webhookUrl) {
    this._webhookUrl = webhookUrl;
  }

  /**
   * Вход пользователя в навык
   */
  async enter() {
    // при заходе в навык, сообщение - пустая строка
    const body = this._buildRequest('');
    return this._sendRequest(body);
  }

  /**
   * Отправка сообщения в навык
   * @param {String} message
   */
  async say(message) {
    const body = this._buildRequest(message);
    return this._sendRequest(body);
  }

  /**
   * Отправка http-запроса
   * @param {Object} body тело запроса
   */
  async _sendRequest(body) {
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    };
    const response = await fetch(this._webhookUrl, {
      method: 'post',
      headers,
      body: JSON.stringify(body),
    });
    const json = await response.json();
    return json.response;
  }
  // ...
};

Тестирующий код для второго кейса выглядит так:


it('should reply the same message', async () => {
  // создаем пользователя
  const user = new User(`http://localhost:${PORT}`);
  // заходим в навык
  await user.enter();
  // отправляем сообщение
  const response = await user.say('что ты умеешь?');
  // проверяем текст ответа
  assert.equal(response.text, 'что ты умеешь?');
});

Запускаем еще раз, и видим что оба теста пройдены:


$ mocha test.js

  ✓ should get hello on enter
  ✓ should reply the same message

  2 passing (37ms)

Дальнейшие шаги


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


Созданную инфраструктуру тестов также можно улучшить:


  • доработать класс User, чтобы можно было менять остальные поля в запросе (например поставить флажок, что у пользователя нет экрана)
  • подключить code-coverage (например nyc)
  • повесить все проверки на pre-commit/pre-push хуки (например с помощью husky)

У меня несколько навыков, поэтому я вынес класс тестового пользователя в отдельный пакет alice-tester, возможно кому-то пригодится.


Полный рабочий код примера из статьи я также выложил на GitHub. Можно склонировать репозиторий и поэкспериментировать.


Спасибо за внимание!

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments 4
Comments Comments 4

Articles