DEV Community

Mad Devs for Mad Devs

Posted on • Edited on

Mad-Fake-Slack — to Test Your Bots Apart from the Real Slack Service (Alpha Version)

Alt Text

mad-fake-slack is primarily a tool for testing your bot, without using real slack servers. In the future, it will be a powerful tool for integration and e2e tests. As you can see, in the gif below, when testing, something can go wrong :). And to avoid this, you need to test not for production, but locally, without reference to real services.

Alt Text
This is how a bot is tested on production))) Thanks for this pleasure to https://www.youtube.com/channel/UCsn6cjffsvyOZCZxvGoJxGg Corridor channel

In most cases, we run integration tests using “real service” because of the lack of analogues of real services.

For the success of such tests, you need the Internet, and if the Internet suddenly disappears, then you will not be able to release the product. In addition, real services do not allow for the reduction of integration testing scenarios.

For example, to pre-fill data, you need to make a lot of work by sending messages, files, and so on before starting the test script. (if your application uses the current data in the channel).

Fake services (how to interpret fake services, you can read here), reduce the testing time, and add the ability to model a specific situation, without many previous ones, manipulations.

If it is necessary for the client to be already “logged in” in the slack client (you will not need to test the client’s login in the slack client), then the fake service allows you to remove unnecessary actions, because you need to test the bot or the application that provides the services.

In this case, mad-fake-slack can help you. While he is able to do a little, but with this functionality, you can test the simplest scenarios to send/receive messages.

Lacks of fake services and any client library

In this world, much is changing, and flying at a tremendous speed and client libraries, instant messengers are often updated by developers. Any copy of the real service will lag behind the real implementation.

And in this case, it all depends on the team, supporting the fake service. But if the company developing the service follow the principle of backward compatibility, then in principle everything should be fine.

After all, even client libraries for Slack, can not 100% guarantee that they will not lag behind the developers of real slack api. So you do not need to worry. Before releasing to production, you will still need to make sure that all functions are working on a staging server. Since the tests do not always guarantee the correctness of the functionality covered by them. Something might slip away from the developer’s view.

What is mad-fake-slack?

  • UI interface is very reminiscent of Slack client

  • The ability to pre-fill data for a channel or a direct channel of a user (through editing files in the db folder) or an application user, since all data is stored on disk in json format. They are easy to fill and change.

  • There is only one user who is considered to be logged in (he is listed first in the user collection)

  • By default, there are two channels: general and random.

  • There is a user named “valera” of the bot application (you can see the user of the application, right after the “Apps” label in the left panel). The username of the application can also be changed if you want, by editing users.json.

Mad-fake-slack disk storage

Alt Text

users.json — is a file that stores data about regular users, bots, and application users.

teams.json — data about teams of the user stored here.

sessions.jsonmd5 hashes of the user’s token and its identifier are stored here. What is it for? Since I did not find any headers when connecting the bot to the mad-fake-slack ws server, it became obvious that you need to somehow understand which user or bot is currently connected or trying to connect to this socket. As a result, such a simple solution was born (approximately similar to the real solution in the Slack). Since the request for the /api/rtm.connect method or /api/rtm.start gives information from the url for the ws server (which the bot connects to), you can pass a hash as a parameter of ws connection url. User can be identified using hash from url parameter at the server-side. But this is the beginning work on security implementation, which in the future can also be useful in testing.

channels.json —here are stored channels data, which can also be easily changed. This list is returned when requesting channels from /api/channels.list

messages — This is the folder where all messages are stored. To simplify and separate access to messages (the “file per channel” structure is used). The channel ID is used as the file name. Thus, in each of the CXXXXXXXX.json files, the messages of the channels are stored separately. Inside each such file is an object which represents messages storage using key-value strategy. The first property of the object stores meta-information and so far only there is a field for storing records with the non-obvious name “last_id” (I will correct this deficiency in the future). The following are the entries in key-value format. Where value is a message object with the required minimum of fields, and the key is the value of the “ts” field — which is guaranteed to be a unique key with the content of the date in unix format + incremental value.

There is also a manager of an improvised database on disk, which allows you to combine data from several files by their keys and give it to the client.

Examples of responses to api requests

The responses folder contains examples of positive responses to api requests with the names of api methods for which they are intended. Now these files are used as blanks of answers, in which the required fields change before sending to the requesting. All sample answers were taken from the documentation site https://api.slack.com/

About UI

User UI based on Handlebars.js — for client rendering and express-handlebars — for server rendering. This is a hybrid rendering since most of the page rendering is done on the server-side. On the client-side is used real-time message reception via ws connection. Server helpers and templates are also used on the client-side, very convenient.

Example of mad-fake-slack UI<br>

The interface is very similar to the slack-client so that it is as usual and convenient to understand the mechanics and how to send messages and so on. Of course not all features it was possible to add to the interface at first, and much functionality still does not available, but to test simple communication with the bot through this interface it is quite possible.

What is now available through the interface?

  • Sending messages to any available channels, direct to the user, direct to application’s user (that is the bot).

  • Receive messages in realtime to the currently active channel.

What needs to be done in the future?

  • Message indication (red circle for an inactive channel with the number of unread messages in it).

  • Parsing messages written in markdown format.

  • Rendering of more complex messages.

  • Testing file uploads.

  • Buttons on messages (for reactions).

  • Displaying of the “typing” status under the text field.

  • Ability to add the channel from UI.

  • Ability to add a new user from the UI.

  • The ability to change the status of the current user.

  • Ability to view RAW messages in JSON format in the channel.

  • Ability to create message threads.

  • Displays message threads.

  • Add sending emoji

  • Deleting user from UI

  • Deleting applications from UI

  • Deleting a channel from UI

  • Channel Archiving from UI

  • Support for slash commands

And so on, in general, the main goal is to expand the possibilities for testing. After all, for any action in the UI, events are generated and the various events and data are called and sent. It will be possible to check accordingly how your bot reacts to these events, to create different situations that in real slack, it would be more difficult to do.

About Server-side

It is based on express.js and express-ws and allows you to handle the following API calls:

  • /api/rtm.connect

  • /api/rtm.start — for now, this method provides exactly as much information as rtm.connect. In the future, the answer will be aligned with the expected documentation in api.

  • /api/auth.test

  • /api/channels.list

  • /api/chat.postMessage

There is also a server-side render of the following pages:

/ — the main page, with the default “general” channel selected and accordingly, its contents.

/messages/:id — when you click on the channel name, the request goes to the server, where the channel ID is parsed and if there is one, the entire page with the displayed channel content will be returned in response. This applies to both direct and open channel and application user.

/api/chat.postMessage — if you disable JS, the form for sending data will still work, since in this case a POST request will be sent to the server in the format of a regular HTTP form and the channel page will be displayed in response, with a new message inside.

About test api

The method that allows you to clear the database in memory is available now.

GET /test/api/db/reset —when sending a request to this URL the data of in-memory database cleared and re-read from the db folder and thus returned to its original state. Now, you only need to refresh the page in your browser (if it was open before).

In the future, the number of methods assisting in integration testing will grow as needed.

About security

There is no check of the token for compliance with what the bot produce when accessing the api or web socket. Verification will be surely added in the future with the corresponding server responses so that it is possible to test unsuccessful authorization.

Real usage example

n the examples folder, there is an example of rtm bot that does not use web hooks, but instead uses the ws connection. First you need to start mad-fake-slack with the npm start command. Then, run the bot by running the npm run example:rtmbot

The bot in the examples is very simple, it receives any message in the channel and immediately sends it to the same channel with some prefix.

Alt Text

What is unusual in implementations of api is weak, in different languages

Here, for example, the official api on node.js https://github.com/slackapi/node-slack-sdk, uses inside the state machine and each request sent to the server contains an id field, which increments by 1. And at the same time, It is required to send the reply_to field in the reply with the id that was sent to the server..

At the same time in the library on python, there is generally no binding to this kind of echo (reply_to field).

I would like to somehow standardize these moments because in the documentation the reply_to field is mentioned in a slightly different accent.

If you’re reconnecting after a network problem this initial set of events may include a response to the last message sent on a previous connection (with a reply_to) so a client can confirm that message was received.

Actually need for reply_to, appears only in case there was a disconnection. But for the node.js version, even simple ping / pong requests must be accompanied by this field. Although it may be that I missed something, but there were no problems with the python version.


Thank you very much, Ankur Oberoi (profile on medium.com) for a very detailed and clear explanation!

In addition to helping with resuming a broken connection, the id field is also used to understand when Slack has acknowledged that the RTM message was received.

When the Node SDK sends a message it returns a Promise immediately. Then later, when Slack sends a message to the client with a matching reply_to, the Node SDK will resolve the corresponding Promise so that the application can sequence work to occur after the message is successfully received by Slack. This is very useful when your application sends a message and would like to know the ts identifier created for that message (maybe you want to store it so you can update that message later).

Here’s an example from the documentation:

const { RTMClient } = require('@slack/rtm-api');
const token = process.env.SLACK_BOT_TOKEN;
const rtm = new RTMClient(token);
// Listen for users who join a channel that the bot user is a member of
// See: https://api.slack.com/events/member_joined_channel
rtm.on('member_joined_channel', async (event) => {
  try {
    // Send a welcome message to the same channel where the 
new member just joined, and mention the user.
    const reply = await rtm.sendMessage(`Welcome to the channel, <@${event.user}>`, event.channel)
console.log('Message sent successfully', reply.ts);
  } catch (error) {
    console.log('An error occurred', error);
  }
});
(async () => {
  await rtm.start();
})();
Enter fullscreen mode Exit fullscreen mode

The Python SDK has a slightly different architecture. It also sends the id field with each RTM message (not only the ping and pong messages). However, the client returns after the message has been sent (but not yet acknowledged by Slack). An exception is the send_over_websocket method which returns a future that the application can await. The returned future will also complete when the message is sent (but not yet acknowledged by Slack).

Link to original comment here

Example of integration tests with jest-puppeeter

The project contains examples of integration tests for the bot in the examples folder. Which test its consistency and adequacy. Examples of all tests can be found here. These tests are run by the command

npm run example:rtmbot:integration

Now 3 test scenarios available

Here are some sample tests:

const { startBot, waitMs, resetDb } = require("./utils");

describe("Bot's greeting", () => {
  let bot = null;

  beforeEach(async () => {
await resetDb();
await page.goto("http://0.0.0.0:9001");
bot = await startBot();
  });

  describe("when starts", () => {
    it("should display greeting", async () => {
  await expect(page).toMatchElement("span.c-message__body", { text: "Hello there! I am a Valera!" });
  const messages = await page.$$eval("span.c-message__body", spans => Array.from(spans).map(el => el.textContent.trim()));
  expect(messages.length).toBe(1);
    });
  });

  afterEach(async () => {
    if (bot) {
  await bot.destroy();
  await waitMs(1000);
  await resetDb();
    };
  });
});
Enter fullscreen mode Exit fullscreen mode

The following example is longer than the previous example and tests the reaction of the bot, if messages are sent to different channels, in this case in “random” and “general”.

const { startBot, waitMs, resetDb } = require("./utils");

describe("Channel communication", () => {
let bot = null;

beforeEach(async () => {
await resetDb();
await page.goto("http://0.0.0.0:9001");
bot = await startBot();
});

describe("general channel", () => {
describe("when user sent message", () => {
it("bot should display echo message", async () => {
await page.keyboard.type("Hello");
await page.keyboard.press("Enter");
await expect(page).toMatchElement("span.c-message__body", { text: "You sent text to the channel: Hello" });
});
});
});

describe("random channel", () => {
describe("when user sent message", () => {
it("bot should display echo message", async () => {
await await Promise.all([
page.waitForNavigation({ waitUntil: "load" }),
expect(page).toClick("span.p-channel_sidebar_name", {
text: "random" })
]);
await page.keyboard.type("Hello from random!");
await page.keyboard.press("Enter");
await expect(page).toMatchElement("span.c-message
body", { text: "You sent text to the channel: Hello from random!" });
const messages = await page.$$eval("span.c-message
_body", spans => Array.from(spans).map(el => el.textContent));
expect(messages.length).toBe(6);
expect(messages[messages.length - 1].trim()).toEqual("You sent text to the channel: Hello from random!");
});
});
});

afterEach(async () => {
if (bot) {
await bot.destroy();
await waitMs(1000);
await resetDb();
};
});
});

Enter fullscreen mode Exit fullscreen mode




Where can you try mad-fake-slack in action?

Right here https://github.com/maddevsio/mad-fake-slack

Conclusion

There is still a lot of work ahead and this is only the first announcement of the testing tool. I hope this tool will be able to identify problems and help in testing in the future. Thank you very much for reading this article!
You may get more content from Mad Devs blog, where technical specialists share their experience, knowledge, and thoughts.

Previously published at maddevs.io.

Top comments (0)