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

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

Для некоего личного проекта мне захотелось написать бота с довольно сложной ветвящейся логикой (например, это может быть система поддержки или диагностики с глубокой вложенностью). При этом граф данной логики имеет огромное количество разветвлений. В общем, быстро стало очевидно, что без автоматизированного тестирования не обойтись — иначе что-то точно упущу из внимания. И насколько же сильно я удивился, когда узнал, что способа тестировать логику ботов просто нет!

Конечно, можно зарегистрировать дополнительного бота для тестирования, но это вариант кривой и некрасивый. Обращение ко внешнему апи во время тестов, заглушка, которая не даст общаться с ботом кому попало, ограничение на скорость отправки сообщений раз в секунду… Если слать сообщение раз в секунду, то граф из каких-то 60 вершин будет тестироваться уже больше минуты! И я уже не говорю о том, что у нас нет никакой возможности смоделировать возросшую нагрузку на бота, при которой он упрётся в ограничение в 30 сообщений в секунду… В общем, я понял, что опять придётся делать что-то своё.

Два решения задачи


Надстройка


Первый вариант с огромной скоростью реализации — это просто надстройка над самой популярной Node.JS библиотекой для реализации Telegram ботов — node-telegram-bot-api. Сделано довольно просто — переписывается обработчик события на отсылку данных, после чего мы обрабатываем данные своим методом и посылаем вручную сгенерированный запрос. Выходит примерно так:

describe('Telegram Test', ()=> {
 const myBot = new TestBot(telegramBot);
 let testChat = 0;

 it('should greet Masha', () => {
   const telegramTest = new TelegramTest(telegramBot);
   testChat++;
   return telegramTest.sendUpdate(testChat, '/ping')
     .then((data)=> {
       if (data.text === 'pong') {
         return telegramTest.sendUpdate(testChat, '/start');
       }
       throw new Error(`Wrong answer for ping! (was  ${data.text})`);
     })
     .then(data=> telegramTest.sendUpdate(testChat, data.keyboard[0][0].text))
     .then((data)=> {
       if (data.text === 'Hello, Masha!') {
         return true;
       }
       throw new Error('Wrong greeting!');
     });
 });
});

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

Свой сервер API Telegram


Решение выше хорошо для быстрого тестирования каких-то простых вещей и покроет большинство потребностей тестирования. Однако что делать, если мы хотим, скажем, тестировать, как наш бот работает под большой нагрузкой? В этом случае единственный логичный выход — это реализовать свой вариант Telegram API. Звучит довольно страшно, но на самом деле нам нужна простая реализация, которая совместима с текущими библиотеками для ботов и позволяет отправлять клиентские запросы. Кстати, как побочная фича — получается, что мы делаем полноценный сервер, с которым можно работать из любого другого стека технологий — хоть питон, хоть C#, хоть что.

To make the long story short, я сделал и такой вариант. С ним тестирование логики того же бота выглядит примерно так:

 it('should greet Masha', function testFull() {
    this.slow(400);
    this.timeout(800);
    let serverConfig = {port: 9000};
    let server = new TelegramServer(serverConfig);
    let token = 'sampleToken';
    let client = server.getClient(token);
    let message = client.makeMessage('/start');
    let telegramBot,
        testBot;
    return server.start()
      .then(()=> client.sendMessage(message))
      .then(()=> {
        let botOptions = {polling: true, baseApiUrl: server.ApiURL};
        telegramBot = new TelegramBot(token, botOptions);
        testBot = new TestBot(telegramBot);
        return client.getUpdates();
      })
      .then((updates)=> {
        console.log(colors.blue(`Client received messages: ${JSON.stringify(updates.result)}`));
        if (updates.result.length !== 1) {
          throw new Error('updates queue should contain one message!');
        }
        let keyboard = JSON.parse(updates.result[0].message.reply_markup).keyboard;
        message = client.makeMessage(keyboard[0][0].text);
        client.sendMessage(message);
        return client.getUpdates();
      })
      .then((updates)=> {
        console.log(colors.blue(`Client received messages: ${JSON.stringify(updates.result)}`));
        if (updates.result.length !== 1) {
          throw new Error('updates queue should contain one message!');
        }
        if (updates.result[0].message.text !== 'Hello, Masha!') {
          throw new Error('Wrong greeting message!');
        }
        return true;
      })
  });

То есть, мы поднимаем у себя на локальном порту сервер, дальше стандартным образом настраиваем бота на работу с этим сервером, и просто шлём ему сообщения от ботов и клиентов. Объект клиента можно получить прямо из сервера, или можно написать своего клиента — сервер просто принимает сообщения в стандартном JSON формате по определённому адресу.

TODO


Конечно, осталось огромное количество всяких полезных вещей, которые можно добавить. К примеру, сейчас очередь сообщений хранится просто в массиве в оперативной памяти. Не реализовано эмулирование таймаутов, и поддерживается работа только с одним клиентом одновременно (сервер просто высылает все сообщения клиенту, который обратился к нему за данными от определённого бота, идентифицируемого по токену). Так же есть поддержка только отправки текстовых сообщений. Связано это с тем, что меня на текущий момент интересует только текст.

Первому проекту особо добавить нечего, а второй скорее всего будет развиваться в сторону моих личных потребностей. Но я немного рассчитываю на поддержку open source сообщества. Лицензия MIT, поддержка Node.js 4 и 6 (5 тоже будет работать, но на ней не пройдут тесты, поскольку используемый для теста бот несовместим с Node 5. Впрочем, я уже создал на это pull request), репозитории ждут ваших пулл реквестов с кодом, оформленным по включённой в проект конфигурации линтера. Некое количество тестов тоже есть. Возможно, со временем будет поддержка ботов и для других мессенджеров.

Если вам интересна сама тема ботов, то оставайтесь на связи. Будет интересно.

Ссылки


  1. Первая реализация (надстройка над node-telegram-bot-api);
  2. Вторая реализация (эмуляция Telegram API);
  3. Хорошая статья про борьбу с ограничениями на скорость отправки сообщений на go;
  4. Официальные ограничения на скорость отправки сообщений.
Поделиться с друзьями
-->

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


  1. stim644
    28.02.2017 12:49

    А если просто отделить модель и тестировать только модель?
    Т.е. будет класс Телеграм и класс модель:

    Telegram {
    Model model;
    sendComand1(String data) { model.sendComand1(data);}
    }


    1. jehy
      28.02.2017 12:51

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


  1. jehy
    28.02.2017 12:51

    не туда.