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 {
String chatId = "test_chat_id";
String message = "test_message";
SendMessage sendMessage = new SendMessage();
sendMessage.setText(message);
sendMessage.setChatId(chatId);
sendMessage.enableHtml(true);
sendBotMessageService.sendMessage(chatId, message);
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() {
Arrays.stream(CommandName.values())
.forEach(commandName -> {
Command command = commandContainer.retrieveCommand(commandName.getCommandName());
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
});
}
@Test
public void shouldReturnUnknownCommand() {
String unknownCommand = "/fgjhdfgdfg";
Command command = commandContainer.retrieveCommand(unknownCommand);
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 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 {
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);
getCommand().execute(update);
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:
The 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:
We update this version in pom.xml and create a new commit:
New commit:
JRTB-3: updated RELEASE_NOTES.md Now push and wait for the build to complete. The build has passed, you can merge it:
I am not deleting the branch, so you can always look and compare what has changed. Our task board has been updated:
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.
GO TO FULL VERSION