JavaRush /Blog Java /Random-PL /Zaimplementujmy Wzorzec Poleceń do pracy z botem. (Część ...
Roman Beekeeper
Poziom 35

Zaimplementujmy Wzorzec Poleceń do pracy z botem. (Część 2) - „Projekt Java od A do Z”

Opublikowano w grupie Random-PL

Piszemy testy dla aplikacji

Początek artykułu: pisanie JRTB-3 . Teraz musimy pomyśleć o testach. Cały dodany kod powinien zostać objęty testami, abyśmy mieli pewność, że funkcjonalność działa tak, jak tego oczekujemy. Najpierw napiszemy testy jednostkowe dla usługi SendBotMessageService.
Test jednostkowy to test sprawdzający logikę małej części aplikacji: zwykle są to metody. Wszystkie połączenia korzystające z tej metody są zastępowane fałszywymi za pomocą prób.
Teraz zobaczysz wszystko. W tym samym pakiecie, tylko w folderze ./src/test/java tworzymy klasę o tej samej nazwie co klasa, którą będziemy testować i na końcu dodajemy Test . Oznacza to, że dla SendBotMessageService będziemy mieli SendBotMessageServiceTest , który będzie zawierał wszystkie testy dla tej klasy. Idea testowania jest następująca: wrzucamy próbnego (fałszywego) JavaRushTelegarmBota, którego następnie pytamy, czy metoda wykonania została wywołana z takim argumentem, czy nie. Oto co się stało:
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);
   }
}
Za pomocą Mockito stworzyłem fałszywy obiekt JavaRushBot, który przekazałem konstruktorowi naszej usługi. Następnie napisałem jeden test (każda metoda z adnotacją Test jest osobnym testem). Struktura tej metody jest zawsze taka sama – nie przyjmuje żadnych argumentów i zwraca wartość void. Nazwa testu powinna informować Cię, co testujemy. W naszym przypadku jest to: powinien poprawnie wysłać wiadomość - musi poprawnie wysłać wiadomość. Nasz test jest podzielony na trzy części:
  • blok //podane - gdzie przygotowujemy wszystko, co niezbędne do testu;
  • block //kiedy – miejsce, w którym uruchamiamy metodę, którą planujemy przetestować;
  • //wtedy blok - gdzie sprawdzamy, czy metoda zadziałała poprawnie.
Ponieważ logika w naszym serwisie jest na razie prosta, wystarczy jeden test dla tej klasy. Teraz napiszmy test dla 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());
   }
}
To nie jest zbyt oczywisty test. Opiera się na logice kontenera. Wszystkie polecenia obsługiwane przez bota znajdują się na liście CommandName i muszą znajdować się w kontenerze. Wziąłem więc wszystkie zmienne CommandName, przeszedłem do Stream API i dla każdej z nich szukałem polecenia z kontenera. Jeśli nie było takiego polecenia, zwrócona zostanie funkcja UnknownCommand. To właśnie sprawdzamy w tej linii:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
Aby sprawdzić, czy UnknownCommand będzie ustawieniem domyślnym, potrzebujesz osobnego testu - ShouldReturnUnknownCommand . Radzę Ci przepisać i przeanalizować te testy. Na razie odbędą się testy półformalne dla drużyn, ale muszą być pisemne. Logika będzie taka sama jak przy testowaniu SendBotMessageService, więc przeniosę ogólną logikę testu do klasy AbstractCommandTest, a każda konkretna klasa testowa zostanie odziedziczona i zdefiniuje potrzebne jej pola. Ponieważ wszystkie testy są tego samego typu, pisanie za każdym razem tego samego nie jest łatwe, a poza tym nie jest to oznaką dobrego kodu. Tak wyglądała uogólniona klasa abstrakcyjna:
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);
   }
}
Jak widać mamy trzy abstrakcyjne metody, po zdefiniowaniu, które z poleceń powinno uruchomić zapisany tutaj test i wykonać się poprawnie. Jest to bardzo wygodne podejście, gdy główna logika znajduje się w klasie abstrakcyjnej, ale szczegóły są zdefiniowane w potomkach. A oto realizacje konkretnych testów:

Test poleceń pomocy:

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

Test NoCommand:

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

Rozpocznij test polecenia:

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

Zatrzymaj test polecenia:

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

Nieznany test poleceń:

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);
   }
}
Jasne jest, że gra była warta świeczki, a dzięki AbstractCommandTest otrzymaliśmy proste i zrozumiałe testy, które łatwo napisać i które są łatwe do zrozumienia. Dodatkowo pozbyliśmy się niepotrzebnego powielania kodu (witaj z zasadą DRY -> Don't Repeat Yourself). Ponadto mamy teraz prawdziwe testy, dzięki którym możemy ocenić wydajność aplikacji. Fajnie byłoby też napisać test na samego bota, ale nie wszystko pójdzie tak łatwo i w ogóle, może gra nie jest warta świeczki, jak to mówią. Dlatego na tym etapie zakończymy nasze zadanie. Ostatnia i ulubiona rzecz - tworzymy commit, pisze wiadomość: JRTB-3: dodano Wzorzec poleceń do obsługi poleceń Telegram Bot I jak zwykle - Github już wie i oferuje utworzenie pull requesta: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 1Kompilacja przeszła i już można połączyć... Ale nie! Zapomniałem zaktualizować wersję projektu i zapisać ją w RELEASE_NOTES. Dodajemy wpis z nową wersją - 0.2.0-SNAPSHOT: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 2Aktualizujemy tę wersję w pom.xml i tworzymy nowe zatwierdzenie: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 3Nowe zatwierdzenie: JRTB-3: zaktualizowano RELEASE_NOTES.md„Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 4 Teraz wciśnij i poczekaj na zakończenie kompilacji. Kompilacja została zakończona, możesz ją scalić: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 5nie usuwam gałęzi, więc zawsze możesz sprawdzić i porównać, co się zmieniło. Nasza tablica zadań została zaktualizowana:„Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 2 - 6

wnioski

Dzisiaj zrobiliśmy wielką rzecz: wprowadziliśmy do pracy szablon Command. Wszystko jest skonfigurowane, a teraz dodanie nowego zespołu będzie prostym i nieskomplikowanym procesem. Rozmawialiśmy dzisiaj także o testach. Nawet trochę pobawiliśmy się w nie powtarzanie kodu w różnych testach dla zespołów. Jak zwykle sugeruję zarejestrowanie się na GitHubie i obserwowanie mojego konta , aby śledzić tę serię i inne projekty, nad którymi tam pracuję. Stworzyłem także kanał telegramowy , w którym będę powielał publikację nowych artykułów. Ciekawostką jest to, że kod wydawany jest zwykle na tydzień przed samym artykułem, a na kanale będę pisać za każdym razem, gdy zostanie wykonane nowe zadanie, co da mi możliwość rozszyfrowania kodu przed przeczytaniem artykułu. Już niedługo będę na bieżąco publikować bota, a ci, którzy zasubskrybują kanał telegramu, jako pierwsi się o tym dowiedzą ;) Dziękuję wszystkim za przeczytanie, ciąg dalszy.

Lista wszystkich materiałów wchodzących w skład serii znajduje się na początku artykułu.

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