JavaRush /Java Blog /Random-TW /讓我們實作命令模式來與機器人一起工作。(第 2 部分)-“Java 專案從頭到尾”
Roman Beekeeper
等級 35

讓我們實作命令模式來與機器人一起工作。(第 2 部分)-“Java 專案從頭到尾”

在 Random-TW 群組發布

我們為應用程式編寫測試

文章開頭:寫JRTB-3。現在我們需要考慮測試。所有新增的程式碼都應該經過測試,以便我們可以確保功能能如我們的預期運作。首先,我們將為 SendBotMessageService 服務編寫單元測試。
單元測試是測試應用程式的一小部分邏輯的測試:通常這些是方法。所有具有此方法的連接都會使用模擬替換為假連接。
現在你會看到一切。在同一個套件中,僅在./src/test/java資料夾中,我們建立一個與我們將測試的類別同名的類,並在末尾添加Test。也就是說,對於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 {
       //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 對象,並將其傳遞給我們服務的建構子。接下來,我編寫了一個測試(帶有 Test 註釋的每個方法都是單獨的測試)。此方法的結構始終相同 - 它不帶參數並傳回 void。測試名稱應該告訴您我們正在測試什麼。在我們的例子中,這是:應該正確發送訊息 - 必須正確發送訊息。我們的測驗分為三個部分:
  • //給定區塊 - 我們準備測試所需的一切;
  • block //when - 我們啟動計劃測試的方法的位置;
  • //then 區塊 - 我們檢查該方法是否正確運作。
由於到目前為止我們服務中的邏輯很簡單,因此對此類進行一次測試就足夠了。現在讓我們為 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,並為每個變數從容器中搜尋了一個命令。如果沒有這樣的命令,則傳回 UnknownCommand。這是我們在這一行中檢查的內容:
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);
   }
}
正如您所看到的,我們有三個抽象方法,定義每個命令後應該執行此處編寫的測試並正確執行。當主要邏輯位於抽象類別中但細節在後代中定義時,這是一種非常方便的方法。事實上,這裡是具體測試的實作:

幫助命令測試:

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 -> Don't Repeat Yourself 原則致敬)。另外,現在我們有真實的測試來判斷應用程式的效能。為機器人本身編寫測試也很好,但一切都不會那麼容易解決,總的來說,也許遊戲得不償失,正如他們所說。因此,在這個階段我們將完成我們的任務。最後也是最喜歡的一件事 - 我們創建一個提交,寫入訊息: JRTB-3:添加了用於處理 Telegram Bot 命令的命令模式 和往常一樣 - Github 已經知道並提供創建拉取請求:「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 1 部分構建已經通過,您已經可以合併...但是不!我忘記更新專案版本並寫在RELEASE_NOTES中。我們新增版本的條目 - 0.2.0-SNAPSHOT:「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 2 部分我們在 pom.xml 中更新此版本並建立新提交:「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 3 部分新提交:JRTB-3:更新的 RELEASE_NOTES.md「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 4 部分現在推送並等待建置完成。建置已經通過,你可以合併它:「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 5 部分我不會刪除分支,所以你可以隨時查看和比較發生了什麼變化。我們的任務板已更新:「Java 專案從頭到尾」:實作用於機器人工作的命令模式。 第 2 - 6 部分

結論

今天我們做了一件大事:我們引入了工作指令範本。一切都已準備就緒,現在新增團隊將是一個簡單明了的過程。我們今天也討論了測試。我們甚至在團隊的不同測試中嘗試不重複程式碼。 像往常一樣,我建議在 GitHub 上註冊並追蹤我的帳戶,以關注本系列以及我在那裡從事的其他專案。我還創建了一個電報頻道,我將複製新文章的發布。一個有趣的事情是,程式碼通常在文章發布前一周發布,每次完成新任務我都會在頻道上寫,這讓我有機會在閱讀文章之前弄清楚程式碼。很快我將持續發布該機器人,訂閱電報頻道的人將是第一個知道它的人;) 感謝大家的閱讀,待續。

此系列所有資料的清單位於本文開頭。

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