JavaRush /Java Blog /Random EN /Let's implement the Command Pattern to work with the bot....

Let's implement the Command Pattern to work with the bot. (Part 2) - "Java project from A to Z"

Published in the Random EN group

We write tests for the application

Beginning of the article: writing JRTB-3 . Now we need to think about testing. All added code should be covered with tests so that we can be sure that the functionality works as we expect. First we will write unit tests for the SendBotMessageService service.
A unit test is a test that tests the logic of some small part of the application: usually these are methods. And all connections that have this method are replaced with fake ones using mocks.
Now you will see everything. In the same package, only in the ./src/test/java folder , we create a class with the same name as the class that we will test, and add Test at the end . That is, for SendBotMessageService we will have SendBotMessageServiceTest , which will contain all the tests for this class. The idea in testing it is as follows: we slip in a mock (fake) JavaRushTelegarmBot, which we then ask whether the execute method was called with such an argument or not. Here's what happened:
package com.github.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.jrtb.bot.JavarushTelegramBot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

@DisplayName("Unit-level testing for SendBotMessageService")
public class SendBotMessageServiceTest {

   private SendBotMessageService sendBotMessageService;
   private JavarushTelegramBot javarushBot;

   @BeforeEach
   public void init() {
       javarushBot = Mockito.mock(JavarushTelegramBot.class);
       sendBotMessageService = new SendBotMessageServiceImpl(javarushBot);
   }

   @Test
   public void shouldProperlySendMessage() throws TelegramApiException {
       //given
       String chatId = "test_chat_id";
       String message = "test_message";

       SendMessage sendMessage = new SendMessage();
       sendMessage.setText(message);
       sendMessage.setChatId(chatId);
       sendMessage.enableHtml(true);

       //when
       sendBotMessageService.sendMessage(chatId, message);

       //then
       Mockito.verify(javarushBot).execute(sendMessage);
   }
}
Using Mockito, I created a mock JavaRushBot object, which I passed to the constructor of our service. Next, I wrote one test (each method with the Test annotation is a separate test). The structure of this method is always the same - it takes no arguments and returns void. The test name should tell you what we are testing. In our case, this is: should properly send message - must send the message correctly. Our test is divided into three parts:
  • block //given - where we prepare everything necessary for the test;
  • block //when - where we launch the method that we plan to test;
  • //then block - where we check whether the method worked correctly.
Since the logic in our service is simple so far, one test for this class will be enough. Now let's write a test for CommandContainer:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Arrays;

@DisplayName("Unit-level testing for CommandContainer")
class CommandContainerTest {

   private CommandContainer commandContainer;

   @BeforeEach
   public void init() {
       SendBotMessageService sendBotMessageService = Mockito.mock(SendBotMessageService.class);
       commandContainer = new CommandContainer(sendBotMessageService);
   }

   @Test
   public void shouldGetAllTheExistingCommands() {
       //when-then
       Arrays.stream(CommandName.values())
               .forEach(commandName -> {
                   Command command = commandContainer.retrieveCommand(commandName.getCommandName());
                   Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
               });
   }

   @Test
   public void shouldReturnUnknownCommand() {
       //given
       String unknownCommand = "/fgjhdfgdfg";

       //when
       Command command = commandContainer.retrieveCommand(unknownCommand);

       //then
       Assertions.assertEquals(UnknownCommand.class, command.getClass());
   }
}
This isn't a very obvious test. It relies on the logic of the container. All commands that the bot supports are in the CommandName list and must be in the container. So I took all the CommandName variables, went to the Stream API and for each one I searched for a command from the container. If there was no such command, the UnknownCommand would be returned. This is what we check in this line:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
And to check that UnknownCommand will be the default, you need a separate test - shouldReturnUnknownCommand . I advise you to rewrite and analyze these tests. There will be semi-formal tests for teams for now, but they need to be written. The logic will be the same as for testing SendBotMessageService, so I will move the general test logic into the AbstractCommandTest class, and each specific test class will be inherited and define the fields it needs. Since all tests are of the same type, writing the same thing every time is not easy, plus this is not a sign of good code. This is how the generalized abstract class turned out:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.bot.JavarushTelegramBot;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.SendBotMessageServiceImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.internal.verification.VerificationModeFactory;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

/**
* Abstract class for testing {@link Command}s.
*/
abstract class AbstractCommandTest {

   protected JavarushTelegramBot javarushBot = Mockito.mock(JavarushTelegramBot.class);
   protected SendBotMessageService sendBotMessageService = new SendBotMessageServiceImpl(javarushBot);

   abstract String getCommandName();

   abstract String getCommandMessage();

   abstract Command getCommand();

   @Test
   public void shouldProperlyExecuteCommand() throws TelegramApiException {
       //given
       Long chatId = 1234567824356L;

       Update update = new Update();
       Message message = Mockito.mock(Message.class);
       Mockito.when(message.getChatId()).thenReturn(chatId);
       Mockito.when(message.getText()).thenReturn(getCommandName());
       update.setMessage(message);

       SendMessage sendMessage = new SendMessage();
       sendMessage.setChatId(chatId.toString());
       sendMessage.setText(getCommandMessage());
       sendMessage.enableHtml(true);

       //when
       getCommand().execute(update);

       //then
       Mockito.verify(javarushBot).execute(sendMessage);
   }
}
As you can see, we have three abstract methods, after defining which each command should run the test that is written here and execute correctly. This is such a convenient approach when the main logic is in an abstract class, but the details are defined in the descendants. And here, in fact, are the implementations of specific tests:

HelpCommandTest:

package com.github.javarushcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.javarushcommunity.jrtb.command.CommandName.HELP;
import static com.github.javarushcommunity.jrtb.command.HelpCommand.HELP_MESSAGE;

@DisplayName("Unit-level testing for HelpCommand")
public class HelpCommandTest extends AbstractCommandTest {

   @Override
   String getCommandName() {
       return HELP.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return HELP_MESSAGE;
   }

   @Override
   Command getCommand() {
       return new HelpCommand(sendBotMessageService);
   }
}

NoCommandTest:

package com.github.javarushcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.javarushcommunity.jrtb.command.CommandName.NO;
import static com.github.javarushcommunity.jrtb.command.NoCommand.NO_MESSAGE;

@DisplayName("Unit-level testing for NoCommand")
public class NoCommandTest extends AbstractCommandTest {

   @Override
   String getCommandName() {
       return NO.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return NO_MESSAGE;
   }

   @Override
   Command getCommand() {
       return new NoCommand(sendBotMessageService);
   }
}

StartCommandTest:

package com.github.javarushcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.javarushcommunity.jrtb.command.CommandName.START;
import static com.github.javarushcommunity.jrtb.command.StartCommand.START_MESSAGE;

@DisplayName("Unit-level testing for StartCommand")
class StartCommandTest extends AbstractCommandTest {

   @Override
   String getCommandName() {
       return START.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return START_MESSAGE;
   }

   @Override
   Command getCommand() {
       return new StartCommand(sendBotMessageService);
   }
}

StopCommandTest:

package com.github.javarushcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.javarushcommunity.jrtb.command.CommandName.STOP;
import static com.github.javarushcommunity.jrtb.command.StopCommand.STOP_MESSAGE;

@DisplayName("Unit-level testing for StopCommand")
public class StopCommandTest extends AbstractCommandTest {

   @Override
   String getCommandName() {
       return STOP.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return STOP_MESSAGE;
   }

   @Override
   Command getCommand() {
       return new StopCommand(sendBotMessageService);
   }
}

UnknownCommandTest:

package com.github.javarushcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.javarushcommunity.jrtb.command.UnknownCommand.UNKNOWN_MESSAGE;

@DisplayName("Unit-level testing for UnknownCommand")
public class UnknownCommandTest extends AbstractCommandTest {

   @Override
   String getCommandName() {
       return "/fdgdfgdfgdbd";
   }

   @Override
   String getCommandMessage() {
       return UNKNOWN_MESSAGE;
   }

   @Override
   Command getCommand() {
       return new UnknownCommand(sendBotMessageService);
   }
}
It’s clear that the game was worth the candle, and thanks to AbstractCommandTest we ended up with simple and understandable tests that are easy to write and easy to understand. In addition, we got rid of unnecessary code duplication (hello to the DRY -> Don't Repeat Yourself principle). In addition, now we have real tests by which we can judge the performance of the application. It would also be nice to write a test for the bot itself, but everything won’t work out so easily and in general, maybe the game is not worth the candle, as they say. Therefore, at this stage we will complete our task. The last and favorite thing - we create a commit, writes the message: JRTB-3: added Command pattern for handling Telegram Bot commands And as usual - Github already knows and offers to create a pull request: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 1The build has passed and you can already merge... But no! I forgot to update the project version and write it in RELEASE_NOTES. We add an entry with the new version - 0.2.0-SNAPSHOT: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 2We update this version in pom.xml and create a new commit: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 3New commit: JRTB-3: updated RELEASE_NOTES.md"Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 4 Now push and wait for the build to complete. The build has passed, you can merge it: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 5I am not deleting the branch, so you can always look and compare what has changed. Our task board has been updated:"Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 6

conclusions

Today we did a big thing: we introduced the Command template for work. Everything is set up, and now adding a new team will be a simple and straightforward process. We also talked about testing today. We even played a little with not repeating the code in different tests for teams. As usual, I suggest registering on GitHub and following my account to follow this series and other projects I'm working on there. I also created a telegram channel in which I will duplicate the release of new articles. One interesting thing is that the code is usually released a week before the article itself, and on the channel I will write every time that a new task has been completed, which will give me the opportunity to figure out the code before reading the article. Soon I will publish the bot on an ongoing basis, and those who subscribe to the telegram channel will be the first to know about it ;) Thank you all for reading, to be continued.

A list of all materials in the series is at the beginning of this article.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION