CodeGym /Java Blog /Toto sisi /關於單元測試:技術、概念、實踐
John Squirrels
等級 41
San Francisco

關於單元測試:技術、概念、實踐

在 Toto sisi 群組發布
今天,您將找不到未進行測試的應用程序,因此這個主題對於新手開發人員來說將比以往任何時候都更相關:沒有測試就無法成功。讓我們考慮原則上使用什麼類型的測試,然後我們將詳細研究有關單元測試的所有知識。 關於單元測試:技術、概念、實踐 - 1

測試類型

什麼是測試?根據維基百科:“軟件測試涉及軟件組件或系統組件的執行,以評估一個或多個感興趣的屬性。” 換句話說,它是在某些情況下檢查我們系統的正確性。好吧,讓我們看看通常有哪些類型的測試:
  • 單元測試——旨在分別檢查系統的每個模塊的測試。這些測試應該應用於系統的最小原子部分,例如模塊。
  • 系統測試——高級測試,用於檢查應用程序的較大部分或整個系統的運行情況。
  • 回歸測試 — 用於檢查新功能或錯誤修復是否影響應用程序現有功能或引入舊錯誤的測試。
  • 功能測試——檢查應用程序的一部分是否滿足規範、用戶故事等中規定的要求。

    功能測試的類型:

    • 白盒測試——在了解系統內部實現的情況下檢查應用程序的一部分是否滿足要求;
    • 黑盒測試——在不知道系統內部實現的情況下檢查應用程序的一部分是否滿足要求。

  • 性能測試 — 編寫的測試以確定係統或系統的一部分在特定負載下的性能。
  • 負載測試 — 旨在檢查系統在標準負載下的穩定性並找出應用程序仍能正常工作的最大負載的測試。
  • 壓力測試 — 旨在檢查應用程序在非標準負載下的性能並確定係統故障前的最大負載的測試。
  • 安全測試——用於檢查系統安全性的測試(來自黑客、病毒、對機密數據的未授權訪問以及其他令人愉快的攻擊)。
  • 本地化測試——應用程序本地化測試。
  • 可用性測試——旨在檢查可用性、可理解性、吸引力和可學習性的測試。
這一切聽起來不錯,但它在實踐中是如何運作的呢?簡單的!我們使用 Mike Cohn 的測試金字塔:關於單元測試:技術、概念、實踐 - 2這是金字塔的簡化版本:它現在被分成更小的部分。但今天我們不會太複雜。我們將考慮最簡單的版本。
  1. 單元——這部分指的是單元測試,它們應用於應用程序的不同層。他們測試應用程序邏輯的最小可分單元。例如,類,但最常見的是方法。這些測試通常會盡可能地嘗試將測試內容與任何外部邏輯隔離開來。也就是說,他們試圖營造應用程序的其餘部分正在按預期運行的錯覺。

    應該總是有很多這樣的測試(比任何其他類型都多),因為它們測試小塊並且非常輕量級,不會消耗大量資源(意味著 RAM 和時間)。

  2. 集成——這部分是指集成測試。此測試檢查系統的較大部分。也就是說,它要么組合幾塊邏輯(幾個方法或類),要么檢查與外部組件交互的正確性。這些測試通常比單元測試小,因為它們更重。

    集成測試的一個示例可以是連接到數據庫並檢查使用它的方法操作的正確性。

  3. UI——這部分指的是檢查用戶界面操作的測試。它們涉及應用程序所有級別的邏輯,這就是它們也被稱為端到端測試的原因。通常,它們的數量要少得多,因為它們最麻煩並且必須檢查最必要(使用)的路徑。

    在上圖中,我們看到三角形的不同部分大小不一:實際工作中不同類型測試的數量所佔比例大致相同。

    今天我們將仔細研究最常見的測試,單元測試,因為所有自尊的 Java 開發人員都應該能夠在基本級別使用它們。

單元測試中的關鍵概念

測試覆蓋率(代碼覆蓋率)是衡量應用程序測試情況的主要指標之一。這是測試覆蓋的代碼百分比 (0-100%)。實際上,許多人都追求這個百分比作為他們的目標。這是我不同意的事情,因為這意味著測試開始應用於不需要的地方。例如,假設我們的服務中有標準的 CRUD(創建/獲取/更新/刪除)操作,沒有額外的邏輯。這些方法是純粹的中介,將工作委託給與存儲庫一起工作的層。在這種情況下,我們沒有什麼可測試的,除了可能給定的方法是否調用 DAO 方法之外,但這是個玩笑。通常使用其他工具來評估測試覆蓋率:JaCoCo、Cobertura、Clover、Emma 等。要更詳細地研究這個主題, TDD代表測試驅動開發。在這種方法中,在執行任何其他操作之前,您編寫一個測試來檢查特定代碼。結果證明這是黑盒測試:我們知道輸入是什麼,我們也知道輸出應該是什麼。這使得可以避免代碼重複。測試驅動開發從為應用程序中的每個功能設計和開發測試開始。在 TDD 方法中,我們首先創建一個測試來定義和測試代碼的行為。TDD 的主要目標是使您的代碼更易於理解、更簡單且無錯誤。關於單元測試:技術、概念、實踐 - 3該方法包括以下內容:
  • 我們編寫測試。
  • 我們運行測試。毫不奇怪,它失敗了,因為我們還沒有實現所需的邏輯。
  • 添加使測試通過的代碼(我們再次運行測試)。
  • 我們重構代碼。
TDD 基於單元測試,因為它們是測試自動化金字塔中最小的構建塊。通過單元測試,我們可以測試任何類的業務邏輯。BDD代表行為驅動的開發。這種方法基於 TDD。更具體地說,它使用通俗易懂的語言示例為參與開發的每個人解釋系統行為。我們不會深入研究這個術語,因為它主要影響測試人員和業務分析師。測試用例是描述檢查被測代碼所需的步驟、具體條件和參數的場景。測試夾具是將測試環境設置為具有成功運行被測方法所必需的狀態的代碼。它是一組預定義的對象及其在指定條件下的行為。

測試階段

測試包括三個階段:
  • 指定測試數據(夾具)。
  • 運行被測代碼(調用被測方法)。
  • 驗證結果並與預期結果進行比較。
關於單元測試:技術、概念、實踐 - 4為確保測試模塊化,您需要與應用程序的其他層隔離。這可以使用存根、模擬和間諜來完成。模擬是可以定制的對象(例如,為每個測試量身定制)。它們讓我們指定我們對方法調用的期望,即預期的響應。我們使用模擬對象來驗證我們是否得到了我們所期望的。存根在測試期間提供對調用的硬編碼響應。它們還可以存儲有關調用的信息(例如,參數或調用次數)。這些有時被稱為間諜。有時人們會混淆存根和模擬這兩個術語:不同之處在於存根不檢查任何東西——它只模擬給定的狀態。模擬是一個有期望的對象。例如,必須調用給定方法一定次數。換句話說,

測試環境

所以,現在進入正題。有幾個可用於 Java 的測試環境(框架)。其中最流行的是 JUnit 和 TestNG。對於此處的審查,我們使用:關於單元測試:技術、概念、實踐 - 5JUnit 測試是類中僅用於測試的方法。該類的名稱通常與其測試的類相同,並在末尾附加“Test”。例如,CarService -> CarServiceTest。Maven 構建系統自動將這些類包含在測試範圍內。其實這個類叫做測試類。讓我們簡要回顧一下基本註釋:

  • @Test 表示該方法是一個測試(基本上,用這個註解標記的方法是一個單元測試)。
  • @Before 表示將在每次測試之前執行的方法。例如,用測試數據填充一個類,讀取輸入數據等。
  • @After 用於標記將在每次測試後調用的方法(例如清除數據或恢復默認值)。
  • @BeforeClass 放在方法之上,類似於@Before。但是這樣的方法在給定類的所有測試之前只被調用一次,因此必須是靜態的。它用於執行更多資源密集型操作,例如啟動測試數據庫。
  • @AfterClass 與@BeforeClass 相反:它對給定的類執行一次,但僅在所有測試之後執行。例如,它用於清除持久性資源或斷開與數據庫的連接。
  • @Ignore 表示一個方法被禁用並且在整個測試運行期間將被忽略。這用於各種情況,例如,如果基本方法已更改並且測試尚未返工以適應更改。在這種情況下,還需要添加描述,即@Ignore("Some description")。
  • @Test(expected = Exception.class) 用於否定測試。這些是驗證方法在出現錯誤時的行為方式的測試,也就是說,測試預計該方法會拋出某種異常。這種方法由 @Test 註釋指示,但指示要捕獲哪個錯誤。
  • @Test(timeout = 100) 檢查該方法是否在不超過 100 毫秒內執行。
  • @Mock 用於在字段上方分配模擬對象(這不是 JUnit 註釋,而是來自 Mockito)。根據需要,我們直接在測試方法中針對特定情況設置模擬的行為。
  • @RunWith(MockitoJUnitRunner.class) 放在一個類之上。這個註釋告訴 JUnit 調用類中的測試。有多種運行程序,包括:MockitoJUnitRunner、JUnitPlatform 和 SpringRunner。在 JUnit 5 中,@RunWith 註釋已被更強大的@ExtendWith 註釋所取代。
讓我們看一下用於比較結果的一些方法:

  • assertEquals(Object expecteds, Object actuals) — 檢查傳遞的對像是否相等。
  • assertTrue(boolean flag) — 檢查傳遞的值是否為真。
  • assertFalse(boolean flag) — 檢查傳遞的值是否為 false。
  • assertNull(Object object) — 檢查傳遞的對像是否為空。
  • assertSame(Object firstObject, Object secondObject) — 檢查傳遞的值是否指向同一個對象。
  • assertThat(T t, 匹配器 匹配器) — 檢查 t 是否滿足 matcher 中指定的條件。
AssertJ 還提供了一個有用的比較方法:assertThat(firstObject).isEqualTo(secondObject)。在這裡我提到了基本方法——其他的是上述方法的變體。

實踐測試

現在讓我們以一個具體的例子來看一下上面的材料。我們將測試服務的更新方法。我們不會考慮 DAO 層,因為我們使用的是默認層。讓我們為測試添加一個啟動器:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
這裡我們有服務類:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
第 8 行——從數據庫中提取更新的對象。第 9-14 行——通過構建器創建一個對象。如果傳入對像有字段,則設置它。如果沒有,我們將保留數據庫中的內容。現在看看我們的測試:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
第 1 行——我們的 Runner。第 4 行——我們通過替換模擬將服務與 DAO 層隔離。第 11 行——我們為類設置了一個測試實體(我們將用作豚鼠的實體)。第 22 行——我們設置服務對象,這就是我們要測試的對象。

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
在這裡我們看到測試分為三個明確的部分: 第 3-9 行 — 指定固定裝置。第 11 行——執行被測代碼。第 13-17 行——檢查結果。更詳細地說:第 3-4 行——設置 DAO 模擬的行為。第 5 行——設置我們將在標準之上更新的實例。第 11 行——使用該方法並獲取結果實例。第 13 行——檢查它是否不為空。第 14 行——比較結果的 ID 和給定的方法參數。第 15 行——檢查名稱是否已更新。第 16 行——查看 CPU 結果。第 17 行——我們沒有在實例中指定這個字段,所以它應該保持不變。我們在這裡檢查該條件。讓我們運行它:關於單元測試:技術、概念、實踐 - 6測試是綠色的!我們可以鬆一口氣了:)綜上所述,測試提高了代碼的質量,使開發過程更加靈活可靠。想像一下重新設計涉及數百個類文件的軟件需要付出多大的努力。當我們為所有這些類編寫單元測試時,我們可以自信地進行重構。最重要的是,它可以幫助我們在開發過程中輕鬆發現錯誤。伙計們,這就是我今天所擁有的。給我一個贊,然後發表評論:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION