TelegramBots/TelegramBots.wiki/abilities/Bot-Testing.md
2018-07-08 01:41:21 +02:00

8.3 KiB

Testing

It is super important to be able to test your bot prior to "release". In this case, release would mean that you're presenting the bot to your designated audience. Nobody likes bots that are buggy, faulty and do clumsy actions. As developers, we appreciate frameworks that provide an ease in testing. Of course, you might no tbe able to catch all bugs that can occur in production, but you'd be far more comfortable in releasing a bot that is well-tested.

Limitations

The issue with the basic API is that all DefaultAbsSender methods (the bot methods you use to send message) are statically defined without interfacing. If you declare your bot and try to do some testing, you won't be able to know that you've executed a method... unless you actually execute it! As an example:

public void sayHello() {
  SendMessage snd = new SendMessage();
  snd.setText("Hello!");
  snd.setChatId(123);
  
  try {
    // We want to test that we actually sent out this message with the contents "Hello!"
    execute(snd);
  } catch (TelegramApiException e) {}
}

This is how you would define a method that says hello in the basic API. How do you go around testing it? If you do attempt to Junit test this method, what will you be testing? If you change the method signature to return the string sent, then you can test the hello message content. However, can you test that you've actually executed the command?

Mock Testing

This section assumes you're familiar with mock testing. Mock testing is basically replacing a real object X with a fake object Y (a mock) of the same type. By doing that, you're able to test whether certain functions were executed.

Obviously, you can't, but there's a twist to it. You can always mock the whole bot, but with that you're also mocking the method sayHello when you actually need its contents and code! We need to extract the bot-sending-specific-methods into their own interface and try to mock that interface instead.

MessageSender Interface

All ability bots declare two utility objects.

The Sender Object

The sender object is an implementation of the MessageSender interface. The interface mirrors all the bot sending methods. A user can supply his own MessageSender, but the AbilityBot module specifies a DefaultSender As you might guess, the default sender is simply a proxy for the bot API methods.

The Silent Object

The silent object is exactly like the sender object, but silent. Its methods return Optional<T>. On exception, it will be an empty optional. The sender object is provided to reduce verboseness of the code (reducing try-catch blocks with something more elegant).

AbilityBot Testing

Let's suppose that you have an ability that says "Hello World!" declared as such:

public Ability saysHelloWorld() {
   return Ability.builder()
       .name("hello")
       .info("Says hello world!")
       .privacy(PUBLIC)
       .locality(ALL)
       .action(ctx -> {
         try{
           sender.execute(new SendMessage().setChatId(ctx.getChatId()).setText("Hello World!"));
         } catch (TelgramApiException e){}
       })
       .build();
 }

The test for this ability would be:

@Test
  public void canSayHelloWorld() {
    Update upd = new Update();
    // Create a new EndUser - EndUser is a class similar to Telegram User, but contains
    // some utility methods like fullName() and shortName() for ease of use
    EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37");
    // This is the context that you're used to, it is the necessary conumer item for the ability
    MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID);

    // We consume a context in the lamda declaration, so we pass the context to the action logic
    bot.saysHelloWorld().action().accept(context);

    // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID
    // The sender here is a mock!
    Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID);
  }

The comments explain every step in the test. In a single assertion with Mockito, we assert that:

  • We've sent the message once
  • The message content was "Hello World!"
  • The message was sent to a specific chat ID

There are some preparations involved before we can perform such a test. Here's the full code snippet for running this test:

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.db.MapDBContext;
import org.telegram.abilitybots.api.objects.EndUser;
import org.telegram.abilitybots.api.objects.MessageContext;
import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.telegrambots.meta.api.objects.Update;

import static org.mockito.Mockito.*;

public class ExampleBotTest {
  public static final int USER_ID = 1337;
  public static final long CHAT_ID = 1337L;

  // Your bot handle here
  private ExampleBot bot;
  // Your sender here
  private MessageSender sender;

  @Before
  public void setUp() {
    // Create your bot
    bot = new ExampleBot();
    // Create a new sender as a mock
    sender = mock(MessageSender.class);
    // Set your bot sender to the mocked sender
    // THIS is the line that prevents your bot from communicating with Telegram servers when it's running its own abilities
    // All method calls will go through the mocked interface -> which would do nothing except logging the fact that you've called this function with the specific arguments
    bot.sender = sender;
  }

  @Test
  public void canSayHelloWorld() {
    Update upd = new Update();
    // Create a new EndUser - EndUser is a class similar to Telegram User, but contains
    // some utility methods like fullName() and shortName() for ease of use
    EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37");
    // This is the context that you're used to, it is the necessary conumer item for the ability
    MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID);

    // We consume a context in the lamda declaration, so we pass the context to the action logic
    bot.saysHelloWorld().action().accept(context);

    // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID
    // The sender here is a mock!
    Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID);
  }
}

DB Abilities

What if the ability performs a DB interaction? We don't want testing procedures to modify the database of the bot.

This is where we differentiate between an online DB and an offline DB. The online DB is the default DB when the bot is instantiated. However, AbilityBot supplies a constructor that reveals a DBContext argument. We can supply another instance of a DB (an offline one) so that the tests don't modify our online DB.

In ExampleBot, we do:

  public ExampleBot(DBContext db) {
    super(BOT_TOKEN, BOT_USERNAME, db);
  }

In ExampleBotTest:

public class ExampleBotTest {
  ...
  
  private DBContext db;
  private MessageSender sender;

@Before
  public void setUp() {
    // Offline instance will get deleted at JVM shutdown
    db = MapDBContext.offlineInstance("test");
    bot = new ExampleBot(db);
    
    ...
  }
  ...

  // We should clear the DB after every test as such
  @After
  public void tearDown() {
    db.clear();
  }
}

Silent Testing

As mentioned before, we also have another object that is able to send messages silently called silent. The constructor of the silent sender requires a MessageSender object. If your abilities use the silent object, be sure to:

public class ExampleBotTest {
  ...
  private DBContext db;
  private MessageSender sender;

@Before
  public void setUp() {
    bot = new ExampleBot(db);
    sender = mock(MessageSender.class);
    bot.silent = new SilentSender(sender);
    ...
  }
  
  ...
}

Do note that in your test assertions, don't use the silent object. Mocked assertion require the mock object. If you recall, the silent object uses the sender object, so your tests will still be correct if you're asserting on the sender object rather than the silent one.