نكتب اختبارات للتطبيق
بداية المقال:
كتابة JRTB-3 . الآن علينا أن نفكر في الاختبار. يجب أن تتم تغطية جميع التعليمات البرمجية المضافة بالاختبارات حتى نتمكن من التأكد من أن الوظيفة تعمل كما نتوقع. أولاً سنكتب اختبارات الوحدة لخدمة SendBotMessageService.
اختبار الوحدة هو اختبار يختبر منطق جزء صغير من التطبيق: عادةً ما تكون هذه طرقًا. ويتم استبدال جميع الاتصالات التي تحتوي على هذه الطريقة بأخرى وهمية باستخدام وهمية. |
الآن سوف ترى كل شيء. في نفس الحزمة، فقط في المجلد
./src/test/java ، قمنا بإنشاء فئة بنفس اسم الفئة التي سنختبرها، وأضفنا
اختبارًا في النهاية . وهذا يعني أنه بالنسبة لـ
SendBotMessageService ، سيكون لدينا
SendBotMessageServiceTest ، والذي سيحتوي على جميع اختبارات هذه الفئة. الفكرة في اختبارها هي كما يلي: نقوم بإدخال JavaRushTelegarmBot وهمي (وهمي)، ونسأل بعد ذلك ما إذا كان تم استدعاء طريقة التنفيذ بمثل هذه الوسيطة أم لا. وهنا ما حدث:
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 وهمي، وقمت بتمريره إلى منشئ خدمتنا. بعد ذلك، كتبت اختبارًا واحدًا (كل طريقة مع التعليق التوضيحي للاختبار هي اختبار منفصل). بنية هذه الطريقة هي نفسها دائمًا - فهي لا تأخذ أي وسيطات وترجع فارغة. يجب أن يخبرك اسم الاختبار بما نختبره. في حالتنا، هذا هو: يجب إرسال الرسالة بشكل صحيح - يجب إرسال الرسالة بشكل صحيح. ينقسم اختبارنا إلى ثلاثة أجزاء:
- الكتلة // المعطاة - حيث نقوم بإعداد كل ما هو ضروري للاختبار؛
- كتلة //متى - حيث نطلق الطريقة التي نخطط لاختبارها؛
- // ثم الحظر - حيث نتحقق مما إذا كانت الطريقة تعمل بشكل صحيح.
وبما أن المنطق في خدمتنا بسيط حتى الآن، فإن اختبار واحد لهذه الفئة سيكون كافيا. لنكتب الآن اختبارًا لـ 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 وبحثت عن أمر من الحاوية لكل متغير. إذا لم يكن هناك مثل هذا الأمر، فسيتم إرجاع UnknownCommand. وهذا ما نتحقق منه في هذا السطر:
Assertions.assertNotEquals(UnknownCommand.class, command.getClass());
وللتحقق من أن UnknownCommand سيكون الإعداد الافتراضي، فأنت بحاجة إلى اختبار منفصل -
mustReturnUnknownCommand . أنصحك بإعادة كتابة هذه الاختبارات وتحليلها. ستكون هناك اختبارات شبه رسمية للفرق في الوقت الحالي، لكن يجب أن تكون كتابية. سيكون المنطق هو نفسه المستخدم في اختبار 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);
}
}
كما ترون، لدينا ثلاث طرق مجردة، بعد تحديد أي أمر يجب أن يقوم بإجراء الاختبار المكتوب هنا وتنفيذه بشكل صحيح. هذا هو النهج المناسب عندما يكون المنطق الرئيسي في فئة مجردة، ولكن يتم تعريف التفاصيل في أحفاد. وهنا، في الواقع، تطبيقات اختبارات محددة:
اختبار أوامر التعليمات:
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);
}
}
اختبار عدم الأمر:
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);
}
}
اختبار الأوامر الأولية:
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);
}
}
اختبار إيقاف الأوامر:
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);
}
}
اختبار أمر غير معروف:
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 -> مبدأ عدم تكرار نفسك). بالإضافة إلى ذلك، لدينا الآن اختبارات حقيقية يمكننا من خلالها الحكم على أداء التطبيق. سيكون من الجيد أيضًا كتابة اختبار للروبوت نفسه، لكن كل شيء لن يعمل بهذه السهولة وبشكل عام، ربما لا تستحق اللعبة كل هذا العناء، كما يقولون. لذلك، في هذه المرحلة سوف نكمل مهمتنا. الشيء الأخير والمفضل - نقوم بإنشاء التزام، يكتب الرسالة:
JRTB-3: تمت إضافة نمط الأمر للتعامل مع أوامر Telegram Bot وكالعادة - يعرف Github بالفعل ويعرض إنشاء طلب سحب:
لقد مر البناء ويمكنك بالفعل دمج...ولكن لا! لقد نسيت تحديث إصدار المشروع وكتابته في RELEASE_NOTES. نضيف إدخالاً بالإصدار الجديد - 0.2.0-SNAPSHOT:
نقوم بتحديث هذا الإصدار في pom.xml وننشئ التزامًا جديدًا:
التزام جديد:
JRTB-3: تحديث RELEASE_NOTES.md الآن ادفع وانتظر حتى يكتمل البناء. لقد مر البناء، يمكنك دمجه:
أنا لا أحذف الفرع، لذلك يمكنك دائمًا البحث ومقارنة ما تغير. تم تحديث لوحة المهام لدينا:
الاستنتاجات
لقد فعلنا اليوم شيئًا كبيرًا: قدمنا قالب الأوامر للعمل. تم إعداد كل شيء، والآن ستكون إضافة فريق جديد عملية بسيطة ومباشرة. تحدثنا أيضًا عن الاختبار اليوم. لقد لعبنا قليلاً مع عدم تكرار الكود في اختبارات مختلفة للفرق.
كالعادة أقترح التسجيل في GitHub ومتابعة حسابي لمتابعة هذه السلسلة والمشاريع الأخرى التي أعمل عليها هناك. لقد قمت أيضًا بإنشاء قناة برقية سأقوم فيها بتكرار إصدار المقالات الجديدة. الشيء المثير للاهتمام هو أن الكود يتم إصداره عادةً قبل أسبوع من المقالة نفسها، وسأكتب على القناة في كل مرة يتم فيها إكمال مهمة جديدة، مما سيمنحني الفرصة لمعرفة الكود قبل قراءة المقال. قريبا سأقوم بنشر البوت بشكل مستمر، ومن يشترك في قناة التليجرام سيكون أول من يعرف عنه ;) شكرا لكم جميعا على القراءة، يتبع.
GO TO FULL VERSION