JavaRush /בלוג Java /Random-HE /בואו ליישם את תבנית הפקודה לעבודה עם הבוט. (חלק 2) - "פרו...
Roman Beekeeper
רָמָה

בואו ליישם את תבנית הפקודה לעבודה עם הבוט. (חלק 2) - "פרויקט ג'אווה מא' עד ת'"

פורסם בקבוצה

אנו כותבים מבחנים עבור האפליקציה

תחילת המאמר: כתיבת JRTB-3 . עכשיו אנחנו צריכים לחשוב על בדיקה. כל הקוד שנוסף צריך להיות מכוסה בבדיקות כדי שנוכל להיות בטוחים שהפונקציונליות פועלת כפי שאנו מצפים. ראשית נכתוב בדיקות יחידה עבור שירות SendBotMessageService.
מבחן יחידה הוא מבחן שבודק את ההיגיון של חלק קטן כלשהו באפליקציה: בדרך כלל אלו שיטות. וכל החיבורים שיש להם שיטה זו מוחלפים באלה מזויפים באמצעות לעג.
עכשיו תראה הכל. באותה חבילה, רק בתיקיית ./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 מדומה, אותו העברתי לבנאי השירות שלנו. לאחר מכן, כתבתי מבחן אחד (כל שיטה עם הערת מבחן היא מבחן נפרד). המבנה של שיטה זו תמיד זהה - היא לא לוקחת טיעונים ומחזירה ריק. שם הבדיקה אמור להגיד לך מה אנחנו בודקים. במקרה שלנו, זה: צריך לשלוח הודעה כראוי - חייב לשלוח את ההודעה בצורה נכונה. המבחן שלנו מחולק לשלושה חלקים:
  • block //given - שבו אנו מכינים את כל הדרוש לבדיקה;
  • block //when - היכן אנו משיקים את השיטה שאנו מתכננים לבדוק;
  • //then block - שם אנו בודקים האם השיטה עבדה כהלכה.
מכיוון שההיגיון בשירות שלנו פשוט עד כה, מבחן אחד למחלקה זו יספיק. כעת נכתוב מבחן עבור 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 ולכל אחד חיפשתי פקודה מהקונטיינר. אם לא הייתה פקודה כזו, הפקודה הבלתי ידועה תוחזר. זה מה שאנחנו בודקים בשורה הזו:
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). בנוסף, כעת יש לנו מבחנים אמיתיים שלפיהם נוכל לשפוט את ביצועי האפליקציה. זה יהיה נחמד גם לכתוב מבחן לבוט עצמו, אבל הכל לא יסתדר כל כך בקלות ובכלל, אולי המשחק לא שווה את הנר, כמו שאומרים. לכן, בשלב זה נשלים את משימתנו. הדבר האחרון והאהוב - אנחנו יוצרים commit, כותבת ההודעה: JRTB-3: הוספה תבנית Command לטיפול בפקודות Telegram Bot וכרגיל - Github כבר יודעת ומציעה ליצור בקשת pull: "פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 1ה-build עבר ואתם כבר יכולים מיזוג... אבל לא! שכחתי לעדכן את גרסת הפרויקט ולכתוב אותה ב-RELEASE_NOTES. אנו מוסיפים ערך עם הגרסה החדשה - 0.2.0-SNAPSHOT: "פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 2אנו מעדכנים את הגרסה הזו ב-pom.xml ויוצרים commit חדש: "פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 3commit חדש: JRTB-3: מעודכן RELEASE_NOTES.md"פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 4 כעת דחוף והמתן להשלמת הבנייה. ה-build עבר, אתה יכול למזג אותו: "פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 5אני לא מוחק את הסניף, אז אתה תמיד יכול להסתכל ולהשוות מה השתנה. לוח המשימות שלנו עודכן:"פרויקט ג'אווה מא' עד ת': הטמעת תבנית פקודה לעבודה עם בוט.  חלק 2 - 6

מסקנות

היום עשינו דבר גדול: הצגנו את תבנית הפקודה לעבודה. הכל מוכן, ועכשיו הוספת צוות חדש יהיה תהליך פשוט ופשוט. דיברנו גם על בדיקות היום. אפילו שיחקנו קצת עם לא לחזור על הקוד במבחנים שונים לקבוצות. כרגיל, אני מציע להירשם ב-GitHub ולעקוב אחרי החשבון שלי כדי לעקוב אחר הסדרה הזו ופרויקטים אחרים שאני עובד עליהם שם. כמו כן, יצרתי ערוץ טלגרם בו אשכפל פרסום של כתבות חדשות. דבר מעניין הוא שבדרך כלל הקוד יוצא שבוע לפני הכתבה עצמה, ובערוץ אכתוב בכל פעם שהסתיימה משימה חדשה, מה שתיתן לי את האפשרות להבין את הקוד לפני קריאת הכתבה. בקרוב אפרסם את הבוט באופן שוטף, ומי שירשם לערוץ הטלגרם יהיה הראשון לדעת על כך ;) תודה לכולכם שקראתם, המשך.

רשימה של כל החומרים בסדרה נמצאת בתחילת מאמר זה.

הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION