אנו כותבים מבחנים עבור האפליקציה
תחילת המאמר:
כתיבת 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 {
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);
}
}
באמצעות 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() {
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());
}
}
זה לא מבחן מאוד ברור. זה מסתמך על ההיגיון של המיכל. כל הפקודות בהן תומך הבוט נמצאות ברשימת 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 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);
}
}
כפי שניתן לראות, יש לנו שלוש שיטות מופשטות, לאחר הגדרת איזו כל פקודה צריכה להריץ את הבדיקה שכתובה כאן ולבצע אותה בצורה נכונה. זו גישה כל כך נוחה כשההיגיון העיקרי הוא בכיתה מופשטת, אבל הפרטים מוגדרים בצאצאים. והנה, למעשה, היישומים של בדיקות ספציפיות:
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:
ה-build עבר ואתם כבר יכולים מיזוג... אבל לא! שכחתי לעדכן את גרסת הפרויקט ולכתוב אותה ב-RELEASE_NOTES. אנו מוסיפים ערך עם הגרסה החדשה - 0.2.0-SNAPSHOT:
אנו מעדכנים את הגרסה הזו ב-pom.xml ויוצרים commit חדש:
commit חדש:
JRTB-3: מעודכן RELEASE_NOTES.md כעת דחוף והמתן להשלמת הבנייה. ה-build עבר, אתה יכול למזג אותו:
אני לא מוחק את הסניף, אז אתה תמיד יכול להסתכל ולהשוות מה השתנה. לוח המשימות שלנו עודכן:
מסקנות
היום עשינו דבר גדול: הצגנו את תבנית הפקודה לעבודה. הכל מוכן, ועכשיו הוספת צוות חדש יהיה תהליך פשוט ופשוט. דיברנו גם על בדיקות היום. אפילו שיחקנו קצת עם לא לחזור על הקוד במבחנים שונים לקבוצות.
כרגיל, אני מציע להירשם ב-GitHub ולעקוב אחרי החשבון שלי כדי לעקוב אחר הסדרה הזו ופרויקטים אחרים שאני עובד עליהם שם. כמו כן, יצרתי ערוץ טלגרם בו אשכפל פרסום של כתבות חדשות. דבר מעניין הוא שבדרך כלל הקוד יוצא שבוע לפני הכתבה עצמה, ובערוץ אכתוב בכל פעם שהסתיימה משימה חדשה, מה שתיתן לי את האפשרות להבין את הקוד לפני קריאת הכתבה. בקרוב אפרסם את הבוט באופן שוטף, ומי שירשם לערוץ הטלגרם יהיה הראשון לדעת על כך ;) תודה לכולכם שקראתם, המשך.
GO TO FULL VERSION