JavaRush /Java Blog /Random EN /Let's implement the Command Pattern for working with the ...

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

Published in the Random EN group

Writing tests for the application

Beginning of the article: writing JRTB-3 . Now we need to think about testing. All added code must be covered by tests so that we can be sure that the functionality works as we expect. First, let's 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 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 we will be testing, and add it at the end of Test . That is, for SendBotMessageService we will have SendBotMessageServiceTest , which will contain all tests for this class. The idea in testing it is as follows: we slip a mock (fake) CodeGymTelegarmBot, which we then ask if the execute method was called with such an argument or not. Here's what happened:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.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 codegymBot;

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

   @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(codegymBot).execute(sendMessage);
   }
}
Using Mockito, I created a mock CodeGymBot object, which I passed to the constructor of our service. Then 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 name of the test should describe 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 run the method that we plan to test;
  • //then block - where we check if 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 on the CommandContainer:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.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 is not 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 searched for a command from the container for each. If there were no such command, an UnknownCommand would be returned. This is what we check in this line:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
And to check that the default will be UnknownCommand, you need a separate test - shouldReturnUnknownCommand . I advise you to rewrite and analyze these tests. There will be semi-formal tests for the teams for now, but they need to be written. The logic will be the same as for testing the SendBotMessageService, so I will move the general logic of the tests into the AbstractCommandTest class, and each specific test class will already be inherited and define the fields it needs. Since all tests are of the same type, writing the same thing every time is not convenient, plus this is not a sign of good code. This is how the generalized abstract class turned out:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.bot.JavarushTelegramBot;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.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 codegymBot = Mockito.mock(JavarushTelegramBot.class);
   protected SendBotMessageService sendBotMessageService = new SendBotMessageServiceImpl(codegymBot);

   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(codegymBot).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 run correctly. This is such a convenient approach when the main logic is in the abstract class, but the details are defined in the descendants. And here, in fact, the implementation of specific tests:

HelpCommandTest:

package com.github.codegymcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.codegymcommunity.jrtb.command.CommandName.HELP;
import static com.github.codegymcommunity.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.codegymcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.codegymcommunity.jrtb.command.CommandName.NO;
import static com.github.codegymcommunity.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);
   }
}

Start Command Test:

package com.github.codegymcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.codegymcommunity.jrtb.command.CommandName.START;
import static com.github.codegymcommunity.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.codegymcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.codegymcommunity.jrtb.command.CommandName.STOP;
import static com.github.codegymcommunity.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.codegymcommunity.jrtb.command;

import org.junit.jupiter.api.DisplayName;

import static com.github.codegymcommunity.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 is clearly seen 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 will not work so easily there, and in general, maybe the game is not worth the candle, as they say. Therefore, at this stage, we will complete our task. Last and favorite - we create a commit, writes a message: JRTB-3: added Command pattern for handling Telegram Bot commands And as usual - the github already knows and suggests creating 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 to RELEASE_NOTES. 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 - 2Update 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 pass. The build passed, you can merge: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 5I don’t delete the branch, so you can always see 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 great job: we implemented the Command template for work. Everything is set up, and now adding a new team will be a simple and straightforward process. Also today today we talked about testing. We even played around a little so as not to repeat the code in different tests for teams. Traditionally, I propose to register on GitHub and subscribe to my account in order to follow this series and other projects that I lead there. I also created a telegram channel, in which I will duplicate the release of new articles. Interestingly, the code usually comes out a week before the article itself, and every time I will write on the channel that a new task has been done, which will make it possible to deal with the code before reading the article. Soon I will publish the bot on a permanent basis, and those who are subscribed to the telegram channel will be the first to know about it;) Thank you all for reading, to be continued."Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 2 - 7

List of all materials in the series at the beginning of this article.

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