知識點
-
單元測試的定義
- 單元測試(Unit Testing)是一種軟件開發的驗證過程,旨在隔離并檢測軟件組件(通常是函數、方法或類)的單個單元的功能是否按照預期執行。每個測試用例驗證特定的條件或功能,確保代碼的每個部分都能獨立地按預期工作。
- 單元測試與集成測試、系統測試的區別。
-
集成測試(Integration Testing):
- 目的:驗證多個單元或組件在一起工作時的交互和協作是否正確。
- 范圍:通常在完成單元測試后進行,關注組件間的接口和交互。
- 特點:可能需要模擬外部系統或使用實際的外部系統。
-
系統測試(System Testing):
- 目的:驗證整個系統的功能是否滿足用戶需求和系統規格。
- 范圍:在所有集成測試完成后進行,涵蓋整個系統。
- 特點:測試用例基于用戶需求和系統規格,可能包括性能測試、安全性測試等。
-
接受測試/驗收測試(Acceptance Testing):
- 目的:確保軟件滿足業務需求和用戶驗收標準。
- 范圍:通常在系統測試之后,由用戶或用戶代表執行。
- 特點:測試用例通常由用戶編寫,關注用戶的工作流程和業務規則。
-
性能測試(Performance Testing):
- 目的:評估軟件應用的速度、響應時間、穩定性、資源消耗等性能指標。
- 范圍:可以是單個組件也可以是整個系統。
- 特點:關注在高負載或特定條件下系統的行為。
-
回歸測試(Regression Testing):
- 目的:確保軟件變更沒有引入新的錯誤。
- 范圍:可以是單個單元、多個組件或整個系統。
- 特點:通常在代碼修改或添加新功能后進行。
-
-
單元測試的目的
- 目的:
- 早期發現錯誤:在開發周期的早期階段捕捉和修復缺陷。
- 提供文檔:測試用例可以作為代碼行為的文檔。
- 支持重構:確保在修改代碼后原有功能仍然正常工作。
-
單元測試的基本原則
-
關鍵特點:
-
獨立性:
- 每個單元測試應該獨立于其他測試運行,不依賴于系統的其他部分或外部環境的狀態。
- 測試不應依賴于數據庫、文件系統、網絡或任何全局狀態。
- 原子性:
- 每個單元測試應該只測試一個具體的功能點或邏輯分支。
- 避免在一個測試中驗證多個功能,以確保測試的明確性和易于理解。
- 可重復性:
- 無論何時何地執行,測試都應該產生相同的結果。
- 這意味著測試不應該依賴于系統狀態或時間敏感的操作。
-
-
測試框架的選擇和介紹
-
JUnit (Java)
- 特點:JUnit是Java語言中最流行的單元測試框架之一,廣泛用于Java和Android應用開發。JUnit 4主要使用注解來標識測試方法,而JUnit 5(Jupiter)則引入了更多的功能和改進。
- 適用場景:適用于Java和Kotlin項目,特別是需要大量自動化測試的企業級應用。
-
TestNG
- 特點:TestNG是JUnit的一個增強版,提供了更多的功能,如參數化測試、并行測試執行等。它同樣使用注解來定義測試方法。
- 適用場景:適用于需要復雜測試配置和并行測試的大型Java應用。
-
pytest (Python)
- 特點:pytest是一個簡單而強大的Python測試框架,支持簡單的斷言和參數化測試,并且可以很容易地與持續集成系統結合。
- 適用場景:適用于Python項目,特別是科學計算和數據分析領域。
-
NUnit (.NET)
- 特點:NUnit是.NET平臺上的一個單元測試框架,受到JUnit的啟發。它支持屬性、斷言和測試運行程序。
- 適用場景:適用于C#、F#和VB.NET項目,特別是需要集成.NET特定功能和特性的應用程序。
-
Mocha (JavaScript/Node.js)
- 特點:Mocha是一個靈活的JavaScript測試框架,適用于Node.js和瀏覽器。它支持異步測試,并且可以與其他斷言庫(如Chai)結合使用。
- 適用場景:適用于JavaScript項目,特別是在需要處理異步操作和Promise的場景。
-
RSpec (Ruby)
- 特點:RSpec是一個用于Ruby語言的行為驅動開發(BDD)框架,提供了一種表達性強的語法來編寫測試。
- 適用場景:適用于Ruby on Rails項目,特別是那些采用BDD方法論的團隊。
-
Google Test (C++)
- 特點:Google Test是一個用于C++的測試框架,提供了豐富的斷言和測試組織功能。
- 適用場景:適用于C++項目,特別是需要高性能和復雜測試結構的應用程序。
-
Karma (JavaScript)
- 特點:Karma是一個測試運行器,可以為JavaScript應用提供實時的測試反饋。它通常與Mocha和Chai等斷言庫一起使用。
- 適用場景:適用于需要在不同瀏覽器上進行測試的JavaScript前端應用。
-
Selenium
- 特點:雖然Selenium主要用于自動化Web瀏覽器交互,但它也可以用來執行Web應用的單元測試。
- 適用場景:適用于Web應用的自動化測試,特別是需要模擬用戶交互的場景。
-
選擇測試框架時的考慮因素:
- 語言和平臺支持:選擇與你的編程語言和開發平臺兼容的框架。
- 社區和文檔:選擇有良好文檔和活躍社區支持的框架,以便在遇到問題時能夠快速找到解決方案。
- 功能需求:根據項目需求選擇提供所需功能的框架,例如參數化測試、并行測試等。
- 集成需求:考慮框架與現有開發工具和持續集成系統的集成能力。
- 團隊熟悉度:選擇團隊成員熟悉或愿意學習的框架,以減少學習曲線。
-
-
編寫測試用例
-
正向測試(Positive Testing)
??????- 定義:驗證程序在正常或預期條件下的行為。
- 方法:
- 確定代碼的正常使用場景。
- 設計測試用例以驗證主要功能和流程。
- 確保測試覆蓋了最常見的使用情況。
-
邊界條件測試(Boundary Value Analysis)
- 定義:驗證程序在邊界或極端條件下的行為。
- 方法:
- 確定輸入或循環的邊界值,如數組的最小長度、最大長度、空數組等。
- 測試循環的開始和結束條件。
- 檢查輸入值的上限和下限。
-
?等價類劃分(Equivalence Partitioning)
- 定義:將輸入數據劃分為若干等價類,每個類的行為預期是相同的。
- 方法:
- 識別有效和無效的輸入值集合。
- 從每個等價類中選擇至少一個測試用例。
-
錯誤猜測(Error Guessing)
- 定義:基于經驗和直覺,猜測可能存在缺陷的代碼區域。
- 方法:
- 識別代碼中可能出錯的邏輯。
- 設計測試用例來驗證這些猜測。
-
測試用例的結構
- 標題:明確測試用例的目的。
- 前提條件:測試執行前必須滿足的條件。
- 測試步驟:詳細描述執行測試的每個步驟。
- 預期結果:定義測試執行后的預期行為或輸出。
- 實際結果:記錄測試執行后的實際行為或輸出。
- 測試狀態:標記測試通過或失敗的狀態。
-
-
斷言的使用
-
斷言的作用:
-
驗證預期結果: 斷言允許測試者明確地指出代碼執行后應該產生的結果。
-
提供明確的錯誤信息: 當測試失敗時,斷言可以提供關于期望值和實際值差異的具體信息,這有助于快速定位問題。
-
簡化測試邏輯: 通過使用斷言,測試代碼可以更加簡潔和直觀,因為斷言通常集成在測試框架中。
-
自動化測試驗證: 斷言使得測試結果的驗證自動化,無需手動檢查輸出。
-
提高測試覆蓋率: 斷言有助于確保測試覆蓋了所有重要的執行路徑和邊界條件。
-
-
相等性斷言:
- 驗證兩個值是否相等。
-
assertEquals(expectedValue, actualValue);
-
不等性斷言:
- 驗證兩個值是否不相等。
-
assertNotEquals(notExpectedValue, actualValue);
-
真值斷言:
- 驗證一個條件是否為真。
-
assertTrue(condition);
-
假值斷言:
- 驗證一個條件是否為假。
-
assertFalse(condition);
-
異常斷言:
- 驗證代碼執行時是否拋出了特定的異常。
-
assertThrows(ExpectedException.class, () -> {// 調用可能拋出異常的方法 });
-
范圍斷言:
- 驗證一個值是否在特定的范圍內。
-
assertGreater(expectedMin, actualValue); assertLess(expectedMax, actualValue);
-
正則表達式斷言:
- 驗證一個字符串是否匹配特定的正則表達式。
-
assertMatchesRegex("expectedPattern", actualString);
-
同一度斷言:
- 驗證兩個引用是否指向同一個對象。
-
assertSame(expectedObject, actualObject);
-
-
測試的組織和管理
-
測試代碼的組織結構
-
按功能組織:
- 測試代碼通常按照被測試的功能模塊組織,每個模塊或類有自己的測試類。
-
目錄結構:
- 在項目中創建一個專門的測試目錄,如
test
或tests
,在這個目錄下進一步按模塊劃分子目錄。
- 在項目中創建一個專門的測試目錄,如
-
測試類和方法:
- 測試類通常以被測試類的名字命名,后跟
Test
作為后綴。 - 測試方法的命名應清晰表達測試的意圖,如
testAdditionPositiveNumbers
。
- 測試類通常以被測試類的名字命名,后跟
-
使用命名空間(針對某些語言):
- 在支持命名空間的語言中,使用命名空間來組織測試代碼,避免命名沖突。
-
模塊化測試代碼:
- 將測試代碼分解為模塊或包,每個模塊包含相關的測試類和輔助類。
-
命名約定:
-
測試類命名:
- 遵循
ClassNameTest
或TestClassName
的命名模式。
- 遵循
-
測試方法命名:
- 使用
test_
前綴,后跟測試的場景或行為描述,如test_addition_with_negative_numbers
。
- 使用
-
常量和變量:
- 測試中使用的常量和變量應有明確和一致的命名規則。
-
避免使用縮寫:
- 在命名測試用例和變量時,為了提高可讀性,避免使用縮寫。
-
測試的管理和執行策略:
-
自動化測試執行:
- 使用持續集成(CI)工具自動執行測試,確保代碼提交后立即驗證。
-
測試依賴管理:
- 確保測試代碼不依賴于特定的運行順序,每個測試都能獨立運行。
-
測試數據管理:
- 使用工廠模式或測試數據構建器模式來管理測試數據,確保數據的一致性和可復用性。
-
測試環境隔離:
- 為測試提供一個隔離的環境,確保測試不會受到外部因素的干擾。
-
測試覆蓋率目標:
- 設定代碼覆蓋率目標,使用工具持續監控測試覆蓋率。
-
測試分層:
- 根據測試的范圍和目的,將測試分層,如單元測試、集成測試、系統測試等。
-
測試報告:
- 生成詳細的測試報告,包括通過率、失敗的測試用例、測試覆蓋率等信息。
-
測試維護:
- 定期審查和更新測試用例,確保它們與代碼的當前狀態保持一致。
-
測試代碼審查:
- 將測試代碼納入代碼審查過程,確保測試的質量。
-
測試優先級:
- 根據風險和重要性為測試用例設置優先級,優先執行關鍵功能的測試。
-
測試版本控制:
- 將測試代碼納入版本控制系統,與應用代碼同步演進。
-
-
Mocking和Stubs
-
Mock對象:
- Mock對象是一個模擬的、假的實現對象,用于在單元測試中代替實際的依賴對象。
- 它主要用于驗證被測試對象的行為,而不是依賴對象的行為。
-
Stubs:
- Stubs(存根)是提供預定響應的簡單對象,通常用于模擬函數或方法的返回值。
- 它們用于設置測試環境,以便在測試中模擬外部依賴的特定行為。
-
應用場景:
- 當單元測試需要隔離外部依賴時,使用Mock對象和Stubs來模擬這些依賴的行為。
- 在測試中驗證對象之間的交互,而不是它們的內部邏輯。
-
使用Mocking工具隔離外部依賴:
假設有一個
UserService
類,它依賴于一個UserRepository
來獲取用戶數據。在單元測試中,我們不想與數據庫交互,而是想模擬UserRepository
的行為:@Test public void testGetUser() {// 創建Mock對象UserRepository mockRepository = Mockito.mock(UserRepository.class);// 設置Mock行為Mockito.when(mockRepository.findById(1)).thenReturn(new User(1, "TestUser"));// 創建被測試對象,并注入Mock的依賴UserService userService = new UserService(mockRepository);// 調用方法并驗證結果User user = userService.getUser(1);assertEquals("TestUser", user.getName());// 驗證交互Mockito.verify(mockRepository).findById(1); }
- 展示如何使用Mocking工具來隔離外部依賴。
-
-
測試覆蓋率
-
重要性:
-
衡量測試的完整性:
- 測試覆蓋率提供了一個量化的指標,用于衡量測試用例覆蓋代碼的程度。
-
發現未測試的代碼:
- 高覆蓋率可以減少未被測試的代碼部分,從而降低引入缺陷的風險。
-
提高代碼質量:
- 通過關注未被測試覆蓋的代碼區域,開發者可以提高代碼的質量和健壯性。
-
使用工具衡量和提高測試覆蓋率:
-
集成覆蓋率工具:
- 在構建過程或持續集成流程中集成覆蓋率工具,如JaCoCo、Cobertura或Istanbul。
-
分析覆蓋率報告:
- 運行測試后,生成覆蓋率報告,分析哪些代碼區域沒有被測試覆蓋。
-
改進測試用例:
- 根據覆蓋率報告,添加或改進測試用例,以覆蓋未測試的代碼。
-
設置覆蓋率目標:
- 為項目設定合理的覆蓋率目標,并持續跟蹤達成情況。
-
持續改進:
- 將覆蓋率作為代碼審查和質量控制的一部分,持續改進測試覆蓋率。
-
實驗?
一 實驗目的:
1、了解什么是單元測試,單元測試的級別、單元測試的內容。
2、掌握單元測試框架JUnit的使用。
3、掌握參數化測試方法的運用及測試腳本的編寫。
二 實驗環境
1、JDK8.0或以上;
2、Intellij IDEA集成開發環境;
3、Maven構建工具。
三 實驗準備
1、掌握JUnit測試框架的基本使用;
2、具備Java編程基礎;
3、安裝及配置好測試環境。
四 實驗內容
(一)網上蛋糕購物系統中,針對蛋糕商品查詢業務的持久類GoodsDao中的getGoodsById、getCountOfGoodsByTypeID方法編寫單元測試類(一般情況下,單元測試要對每個方法進行測試)。
(1)創建GoodsDaoTest測試類,編寫對應的測試方法。請提供GoodsDaoTest測試類代碼,要求代碼中要對每個方法進行注釋說明。
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;public class GoodsDaoTest {@Mockprivate GoodsDataAccess goodsDataAccess; // 假設這是訪問數據庫的接口@InjectMocksprivate GoodsDao goodsDao; // 被測試的持久類@BeforeEachpublic void setUp() {MockitoAnnotations.initMocks(this); // 初始化Mock對象}// 測試getGoodsById方法@Testpublic void testGetGoodsById_WithValidId_ShouldReturnCorrectGoods() {// 準備int validId = 1;Goods expectedGoods = new Goods(validId, "Chocolate Cake", 20.0);Mockito.when(goodsDataAccess.getGoodsById(validId)).thenReturn(expectedGoods);// 執行Goods result = goodsDao.getGoodsById(validId);// 驗證assertNotNull(result, "返回的對象不應為空");assertEquals(expectedGoods, result, "返回的蛋糕應與預期相符");}@Testpublic void testGetGoodsById_WithInvalidId_ShouldReturnNull() {// 準備int invalidId = -1;Mockito.when(goodsDataAccess.getGoodsById(invalidId)).thenReturn(null);// 執行Goods result = goodsDao.getGoodsById(invalidId);// 驗證assertNull(result, "使用無效ID查詢時,應返回null");}// 測試getCountOfGoodsByTypeID方法@Testpublic void testGetCountOfGoodsByTypeID_WithValidTypeId_ShouldReturnCorrectCount() {// 準備int validTypeId = 1;int expectedCount = 10;Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(validTypeId)).thenReturn(expectedCount);// 執行int result = goodsDao.getCountOfGoodsByTypeID(validTypeId);// 驗證assertEquals(expectedCount, result, "返回的數量應與預期相符");}@Testpublic void testGetCountOfGoodsByTypeID_WithInvalidTypeId_ShouldReturnZero() {// 準備int invalidTypeId = -1;Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(invalidTypeId)).thenReturn(0);// 執行int result = goodsDao.getCountOfGoodsByTypeID(invalidTypeId);// 驗證assertEquals(0, result, "使用無效類型ID查詢時,應返回0");}
}
代碼解釋:
@Mock
注解用于創建模擬對象。@InjectMocks
注解用于創建被測試類的實例,并將模擬對象注入到它的依賴中。@BeforeEach
注解的方法在每個測試方法執行之前都會運行,用于設置測試環境。@Test
注解的方法是實際的測試用例。Mockito.when(...).thenReturn(...)
用于定義模擬對象的行為。assertNotNull
、assertEquals
和assertNull
是JUnit提供的斷言方法,用于驗證測試結果是否符合預期。
?
(2)請設計3條測試用例測試getCountOfGoodsByTypeID方法,執行測試。
測試用例:
序號 | 測試用例編號 | 測試用例名稱 | 輸入數據 | 預期結果 | 測試結果 |
測試用例1: 有效商品類型ID
序號 | 測試用例編號 | 測試用例名稱 | 輸入數據 | 預期結果 | 測試結果 |
---|---|---|---|---|---|
1 | TC001 | 正常商品類型查詢 | 1 | >0 | [待測試] |
說明:此測試用例驗證當提供有效的商品類型ID時,方法應返回該類型下商品的正數數量。
測試用例2: 無效商品類型ID(負數)
序號 | 測試用例編號 | 測試用例名稱 | 輸入數據 | 預期結果 | 測試結果 |
---|---|---|---|---|---|
2 | TC002 | 負數商品類型查詢 | -1 | 0 | [待測試] |
說明:此測試用例驗證當商品類型ID為負數時,方法應返回0,表示沒有商品匹配。
測試用例3: 邊界商品類型ID(例如最大的正整數)
序號 | 測試用例編號 | 測試用例名稱 | 輸入數據 | 預期結果 | 測試結果 |
---|---|---|---|---|---|
3 | TC003 | 最大正整數商品類型查詢 | Integer.MAX_VALUE | 0或實際數量 | [待測試] |
說明:此測試用例驗證當商品類型ID為整數最大值時,方法的表現,可能是返回0或者實際的商品數量,取決于數據庫中的數據。
?
(3)針對以上測試方法的不足(多條測試用例需要多次執行),根據參數化測試的規則,使用以上測試用例數據,修改測試方法并執行測試。請提供修改后的測試方法代碼
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;public class GoodsDaoTest {@Mockprivate GoodsDataAccess goodsDataAccess;@InjectMocksprivate GoodsDao goodsDao;@BeforeEachpublic void setUp() {MockitoAnnotations.initMocks(this);}// 參數化測試用例@ParameterizedTest@CsvSource({"1, 10, 應返回正數的商品數量","-1, 0, 應返回0表示沒有商品","2147483647, 5, 應返回實際的商品數量或0"})public void testGetCountOfGoodsByTypeID(int typeId, int expectedResult, String caseDescription) {// 準備Mockito.when(goodsDataAccess.getCountOfGoodsByTypeID(typeId)).thenReturn(expectedResult);// 執行int result = goodsDao.getCountOfGoodsByTypeID(typeId);// 驗證assertEquals(expectedResult, result, caseDescription);}
}
- 新建一個Foo類,使用Mockito框架對該類進行測試
public class Foo {
????private Bar bar;
????public void setBar(Bar bar) {
????????this.bar = bar;
????}
????public String doSomething() {
????????return "Foo::doSomething " + bar.doSomethingElse();
????}
}
public class Bar {
????public String doSomethingElse() {
????????return "Bar::doSomethingElse";
}
}
測試腳本截圖為:
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;public class FooTest {@Mockprivate Bar mockBar; // 創建Bar的Mock對象@InjectMocksprivate Foo foo; // 被測試的Foo對象@BeforeEachpublic void setUp() {// 初始化Mock對象MockitoAnnotations.openMocks(this);}@Testpublic void testDoSomething() {// 準備when(mockBar.doSomethingElse()).thenReturn("Bar::doSomethingElse");// 執行String result = foo.doSomething();// 驗證verify(mockBar).doSomethingElse(); // 驗證Bar的doSomethingElse方法被調用assertEquals("Foo::doSomething Bar::doSomethingElse", result);}
}
五 實驗總結
(1)什么叫樁程序?樁程序有什么作用?
樁程序(Stub): 樁程序是一種模擬對象,用于在單元測試中代替實際的依賴項或外部系統。它通常用于模擬那些在測試環境中不可用或不適合使用的組件,如數據庫、網絡服務或復雜計算。
作用:
- 隔離測試:樁程序允許開發者在不依賴外部系統或復雜邏輯的情況下測試代碼。
- 控制測試環境:通過返回預定的響應,樁程序可以控制測試環境,確保測試的一致性和可重復性。
- 提高測試速度:使用樁程序可以避免耗時的外部調用,從而加快測試執行速度。
- 簡化測試邏輯:樁程序可以簡化測試邏輯,使測試用例更專注于驗證被測試代碼的行為。
- 模擬錯誤情況:樁程序還可以模擬錯誤情況或異常響應,幫助測試代碼的健壯性和錯誤處理能力。
(2)使用參數化測試有什么好處?
參數化測試是一種測試方法,它允許使用不同的輸入參數多次執行同一個測試方法。以下是使用參數化測試的一些好處:
- 減少重復代碼:參數化測試可以減少編寫和維護多個相似測試用例的代碼量。
- 提高測試效率:通過一次性執行多個測試用例,參數化測試可以提高測試的效率和覆蓋率。
- 簡化測試數據管理:集中管理測試數據,便于更新和維護。
- 增強測試的靈活性:可以輕松地添加、修改或刪除測試參數,以適應不同的測試需求。
- 提高測試的可讀性:通過清晰的參數化表達,測試用例的意圖和行為更容易被理解。
- 支持邊界值分析:參數化測試可以方便地測試邊界值和異常值,提高代碼的邊界條件覆蓋率。
- 自動化測試執行:參數化測試通常與自動化測試框架結合使用,實現測試的自動化執行。
- 易于維護和擴展:隨著軟件需求的變更,參數化測試可以更容易地進行更新和擴展。
- 提供一致的測試結果:確保每個測試用例在相同的測試邏輯下運行,提供一致的測試結果。