Пишем тесты к приложению
Начало статьи:
пишем JRTB-3.
Теперь нужно подумать о тестировании. Весь добавленный код должен быть покрыт тестами, чтобы мы были уверены, что функциональность работает как мы ожидаем.
Первыми напишем unit-тесты для сервиса SendBotMessageService.
Unit-тест — это тест, который проверяет логику какой-то маленькой части приложения: обычно это методы. А все связи, у которых есть этот метод, заменяются на ненастоящие при помощи моков. |
Сейчас вы все увидите. В том же пакете, только уже в папке
./src/test/java создаем класс с таким же именем, как у класса, который будем тестировать, и добавляем в конце
Test. То есть для
SendBotMessageService у нас будет
SendBotMessageServiceTest, в котором будут все тесты на этот класс.
Идея в его тестировании следующая: мы подсовываем моковый (фейковый) JavaRushTelegarmBot, у которого потом спросим, вызывался ли метод execute с таким аргументом или нет.
Вот что получилось:
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);
}
}
При помощи Mockito я создал моковый объект JavaRushBot, который передал в конструктор нашему сервису.
Далее написал один тест (каждый метод с аннотацией Test — это отдельный тест). Структура этого метода одна и та же всегда — он не принимает аргументы, и возвращает void. Имя теста должно рассказать о том, что мы тестируем. В нашем случае это:
should properly send message — должен правильно отправить сообщение.
Тест у нас поделен на три части:
- блок //given — где мы подготавливаем все необходимое к тесту;
- блок //when — где запускаем тот метод, который планируем тестировать;
- блок //then — где мы проверяем, правильно ли отработал метод.
Так как пока что логика в нашем сервисе простая, одного теста для этого класса будет достаточно.
Теперь напишем тест на 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());
}
}
Здесь не совсем очевидный тест. Он опирается на логику работы контейнера. Все команды, которые поддерживает бот, находятся в списке CommandName и должны быть в контейнере. Поэтому я взял все переменные CommandName, перешел в Stream API и для каждого выполнил поиск команды из контейнера. Если бы такой команды не было, была бы возвращена команда UnknownCommand.
Это мы и проверяем в этой строке:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
А чтобы проверить, что по умолчанию будет UnknownCommand, нужен отдельный тест —
shouldReturnUnknownCommand.
Советую эти тесты переписать и проанализировать.
Для команд пока что будут полуформальные тесты, но их нужно писать. Логика будет такая же, как и для тестирования SendBotMessageService, поэтому я вынесу общую логику тестов в AbstractCommandTest класс, и уже каждый конкретный тест-класс будет наследоваться и определять необходимые ему поля.
Так как все тесты однотипные, писать одно и тоже каждый раз не с руки, плюс это не признак хорошего кода. Вот такой получился обобщенный абстрактный класс:
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);
}
}
Как видим, у нас есть три абстрактных метода, после определения которых у каждой команды должен запуститься тест, который здесь написан, и выполнится правильно.
Это такой удобный подход, когда основная логика находится в абстрактном классе, а вот детали определяются в наследниках. А вот, собственно, реализации конкретных тестов:
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);
}
}
Отлично видно, что игра стоила свеч, и благодаря AbstractCommandTest мы получили в итоге простые и понятные тесты, которые легко писать, легко понимать. В довесок избавились от лишнего дублирования кода (привет принципу DRY -> Don’t Repeat Yourself).
К тому же теперь у нас есть настоящие тесты, по которым можно судить о работе приложения.
Еще хорошо бы написать тест на самого бота, но там все так просто не получится и вообще, может, игра не стоит свеч, как говорится. Поэтому на данном этапе будем завершать нашу задачу.
Последнее и любимое — создаем коммит, пишет сообщение:
JRTB-3: added Command pattern for handling Telegram Bot commands
И как обычно — гитхаб уже знает и предлагает создать пулл-реквест:
Билд прошел и можно уже мержить… Но нет!
Я ж забыл обновить версию проекта и записать в RELEASE_NOTES. Добавляем запись с новой версией — 0.2.0-SNAPSHOT:
Обновляем эту версию в pom.xml и создаем новый коммит:
Новый коммит:
JRTB-3: updated RELEASE_NOTES.mdТеперь пуш и ждем пока пройдет билд.
Билд прошел, можно и мержить:
Ветку я не удаляю, так что всегда можно будет посмотреть и сравнить, что изменилось.
Наша доска с задачами обновилась:
Выводы
Сегодня мы сделали большое дело: внедрили Command шаблон для работы. Все настроено, и теперь добавление новой команды будет простым и понятным процессом.
Также сегодня сегодня поговорили о тестировании. Немного даже поигрались с тем, чтобы не повторять код в разных тестах для команд.
Традиционно предлагаю зарегистрироваться на GitHub и подписаться на мой аккаунт, чтобы следить за этой серией и другими проектами, которые я там веду.
Также я создал телеграм-канал, в котором буду дублировать выход новых статей. Из интересного — код обычно выходит на неделю раньше самой статьи, и на канале я буду каждый раз писать о том, что новая задача сделана, что даст возможность разобраться с кодом до прочтения статьи.
В скором времени я опубликую бота на постоянной основе, и первым узнают об этом те, кто подписан на телеграмм канал ;)
Всем спасибо за прочтение, продолжение следует.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ