JavaRush /Java блог /Random UA /Реалізуємо Command Pattern для роботи з роботом. (Частина...
Roman Beekeeper
35 рівень

Реалізуємо Command Pattern для роботи з роботом. (Частина 2) - "Java-проект від А до Я"

Стаття з групи Random UA

Пишемо тести до додатку

Початок статті: пишемо JRTB-3 . Тепер слід подумати про тестування. Весь доданий код повинен бути покритий тестами, щоб ми були впевнені, що функціональність працює, як ми очікуємо. Першими напишемо unit-тести для сервісу SendBotMessageService.
Unit-тест - це тест, який перевіряє логіку якоїсь маленької частини програми: зазвичай це методи. А всі зв'язки, які мають цей метод, замінюються на несправжні за допомогою моків.
Зараз ви побачите все. У тому ж пакеті, тільки вже в папці ./src/test/java створюємо клас з таким самим ім'ям, як у класу, який тестуватимемо, і додаємо в кінці Test . Тобто для SendBotMessageService ми матимемо SendBotMessageServiceTest , в якому будуть всі тести на цей клас. Ідея в його тестуванні наступна: ми підсовуємо моковий (фейковий) CodeGymTelegarmBot, у якого потім спитаємо, чи викликався метод execute з таким аргументом чи ні. Ось що вийшло:
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);
   }
}
За допомогою Mockito я створив моковий об'єкт CodeGymBot, який передав конструктор нашому сервісу. Далі написав один тест (кожен метод із анотацією Test – це окремий тест). Структура цього методу одна й та сама завжди - він не приймає аргументи, і повертає void. Ім'я тесту має розповісти, що ми тестуємо. У нашому випадку це: should properly send message - має правильно надіслати повідомлення. Тест у нас поділений на три частини:
  • блок //given - де ми готуємо все необхідне тесту;
  • блок //when - де запускаємо той метод, який плануємо тестувати;
  • блок //then - де ми перевіряємо, чи правильно відпрацював метод.
Оскільки поки що логіка у нашому сервісі проста, одного тесту для цього класу буде достатньо. Тепер напишемо тест на 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());
   }
}
Тут не зовсім очевидний тест. Він спирається на логіку роботи контейнера. Усі команди, які підтримують бот, перебувають у списку CommandName і мають бути у контейнері. Тому я взяв усі змінні CommandName, перейшов у Stream API і для кожного здійснив пошук команди з контейнера. Якби такої команди не було, було б повернено команду UnknownCommand. Це ми і перевіряємо у цьому рядку:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
А щоб перевірити, що за замовчуванням буде UnknownCommand, потрібен окремий тест - shouldReturnUnknownCommand . Раджу ці тести переписати та проаналізувати. Для команд поки що будуть напівформальні випробування, але їх треба писати. Логіка буде така ж, як і для тестування SendBotMessageService, тому я винесу загальну логіку тестів до AbstractCommandTest класу, і вже кожен конкретний тест-клас успадковуватиметься і визначатиме необхідні йому поля. Так як всі тести однотипні, писати одне і теж щоразу не з руки, плюс це не ознака хорошого коду. Ось такий вийшов узагальнений абстрактний клас:
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);
   }
}
Як бачимо, у нас є три абстрактні методи, після визначення яких кожна команда має запуститися тест, який тут написаний, і виконається правильно. Це такий зручний підхід, коли основна логіка знаходиться в абстрактному класі, а ось деталі визначаються спадкоємцями. А ось, власне, реалізації конкретних тестів:

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);
   }
}

StartCommandTest:

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);
   }
}
Відмінно видно, що гра коштувала свічок, і завдяки AbstractCommandTest ми отримали в результаті прості та зрозумілі тести, які легко писати, легко розуміти. На додаток позбулися зайвого дублювання коду (привіт принципу DRY -> Don't Repeat Yourself). До того ж, тепер у нас є справжні тести, за якими можна судити про роботу програми. Ще добре б написати тест на самого бота, але там все так просто не вийде і взагалі, може, гра не варта свічок, як то кажуть. Тому на даному етапі завершуватимемо наше завдання. Останнє і улюблене - створюємо коміт, пише повідомлення: JRTB-3: І як зазвичай - гітхаб вже знає і пропонує створити пулл-реквест:"Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 1Білд пройшов і вже можна мержить… Але ні! Я ж забув оновити версію проекту та записати у RELEASE_NOTES. Додаємо запис з новою версією - 0.2.0-SNAPSHOT: "Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 2Оновлюємо цю версію в pom.xml і створюємо новий коміт: "Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 3Новий комміт: JRTB-3: updated RELEASE_NOTES.md"Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 4 Тепер пуш і чекаємо поки пройде білд. Білд пройшов, можна й мержити: "Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 5Гілку я не видаляю, тож завжди можна буде подивитися і порівняти, що змінилося. Наша дошка із завданнями оновилася:"Java-проект від А до Я": Реалізуємо Command Pattern для роботи з роботом.  Частина 2 - 6

Висновки

Сьогодні ми зробабо велику справу: запровадабо Command шаблон для роботи. Все налаштовано, і тепер додавання нової команди буде простим та зрозумілим процесом. Також сьогодні сьогодні поговорабо про тестування. Трохи навіть погралися для того, щоб не повторювати код у різних тестах для команд. Традиційно пропоную зареєструватися на GitHub і підписатися на мій обліковий запис , щоб стежити за цією серією та іншими проектами, які я там веду. Також я створив телеграм-канал, в якому дублюватиму вихід нових статей. З цікавого — код зазвичай виходить на тиждень раніше за саму статтю, і на каналі я щоразу писатиму про те, що нове завдання зроблено, що дасть можливість розібратися з кодом до прочитання статті. Незабаром я опублікую бота на постійній основі, і першим дізнаються про це ті, хто підписаний на телеграм канал ;) Всім дякую за прочитання, продовження слідує.

Список всіх матеріалів серії на початку цієї статті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ