1. JUnit 最佳實踐指南
原文: https://howtodoinjava.com/best-practices/unit-testing-best-practices-junit-reference-guide/
我假設您瞭解 JUnit 的基礎知識。 如果您沒有基礎知識,請首先閱讀(已針對 JUnit 5 更新)。 現在,我們將介紹在編寫測試用例時必須考慮的 junit 最佳實踐。
編寫糟糕的單元測試非常容易,這會給專案增加很少的價值,同時又會天文數字地增加程式碼更改的成本。
Table of Contents
Unit testing is not about finding bugs
Tips for writing great unit tests
Test only one code unit at a time
Don’t make unnecessary assertions
Make each test independent to all the others
Mock out all external services and state
Don’t unit-test configuration settings
Name your unit tests clearly and consistently
Write tests for methods that have the fewest dependencies first, and work your way up
All methods, regardless of visibility, should have appropriate unit tests
Aim for each unit test method to perform exactly one assertion
Create unit tests that target exceptions
Use the most appropriate assertion methods.
Put assertion parameters in the proper order
Ensure that test code is separated from production code
Do not print anything out in unit tests
Do not use static members in a test class
Do not write your own catch blocks that exist only to fail a test
Do not rely on indirect testing
Integrate Testcases with build script
Do not skip unit tests
Capture results using the XML formatter
Summary
在程式設計中,“單元測試是一種測試原始碼的各個單元以確定它們是否適合使用的方法。” 現在,這個原始碼單元可以在不同的情況下使用。
例如:在過程程式設計中,一個單元可以是一個完整的模組,但更常見的是一個單獨的函式或過程。 在物件導向程式設計中,一個單元通常是一個完整的介面,例如一個類,但可以是一個單獨的方法。 直觀上,應該將一個單元視為應用中最小的可測試部分。
單元測試不是尋找回歸錯誤
好吧,瞭解單元測試的動機很重要。 單元測試不是查詢應用範圍的錯誤或檢測迴歸缺陷的有效方法。 根據定義,單元測試將分別檢查程式碼的每個單元。 但是,當您的應用實際執行時,所有這些單元都必須協同工作,並且整個過程比獨立測試部分的總和更加複雜和微妙。 證明 X 和 Y 元件都可以獨立工作,並不表示它們相互相容或配置正確。
因此,如果您要查詢回歸錯誤,則實際上一起執行整個應用會更有效,因為它將在生產環境中執行,就像手動測試 。 如果您將這種測試自動化以檢測將來發生的破損,則稱為整合測試,通常使用與單元測試不同的技術。
“從本質上講,單元測試應該被視為設計過程的一部分,因為它是 TDD(測試驅動開發)中的一部分。” 這應該用於支援設計過程,以便設計人員可以識別系統中的每個最小模組並分別進行測試。
編寫出色的單元測試的提示
1.一次僅測試一個程式碼單元
首先,也許是最重要的。 當我們嘗試測試程式碼單元時,該單元可能有多個用例。 我們應該始終在單獨的測試用例中測試每個用例。 例如,如果我們為一個函式編寫測試用例,該函式應該具有兩個引數,並且在進行一些處理後應返回一個值,那麼不同的用例可能是:
- 第一個引數可以為空。 它應該丟擲無效的引數異常。
- 第二個引數可以為空。 它應該丟擲無效的引數異常。
- 兩者都可以為空。 它應該丟擲無效的引數異常。
- 最後,測試功能的有效輸出。 它應該返回有效的預定輸出。
當您進行了一些程式碼更改或重構,然後測試功能沒有損壞時,這將很有幫助,執行測試用例就足夠了。 同樣,如果您更改任何行為,則需要更改單個或最少數量的測試用例。
2.不要做出不必要的斷言
請記住,單元測試是某種行為應該如何工作的設計規範,而不是對程式碼碰巧所做的所有事情的觀察列表。
不要試圖斷言所有事情都只專注於要測試的內容,否則,由於一個原因,您最終將導致多個測試用例失敗,這無助於實現任何目標。
3.使每個測試獨立於其他所有測試
不要製作單元測試用例鏈。 這將阻止您確定測試用例失敗的根本原因,並且您將不得不除錯程式碼。 同樣,它會建立依賴關係,這意味著如果您必須更改一個測試用例,那麼您就不必要在多個測試用例中進行更改。
嘗試對所有測試用例使用@Before
和@After
方法。 如果您需要多種支援@Before
或@After
中的不同測試用例的方法,請考慮建立新的Test
類。
4.模擬所有外部服務和狀態
否則,這些外部服務中的行為會與多個測試重疊,並且狀態資料意味著不同的單元測試會影響彼此的結果。 如果您必須按照特定的順序執行測試,或者只有在資料庫或網路連線處於活動狀態時才能執行,則您肯定走錯了路。
另外,這很重要,因為您不希望除錯由於某些外部系統中的錯誤而實際失敗的測試用例。
(順便說一句,有時您的架構可能意味著您的程式碼在單元測試期間會觸及靜態變數。如果可以,請避免這樣做,但如果不能這樣做,請至少確保每個測試在執行之前將相關的靜態操作重置為已知狀態。 )
5.不進行單元測試配置設定
根據定義,您的配置設定不是任何程式碼單元的一部分(這就是為什麼您將設定提取到某些屬性檔案中的原因)。 即使您可以編寫用於檢查配置的單元測試,也可以只編寫一個或兩個測試用例,以驗證配置載入程式碼是否正常工作,僅此而已。
在每個單獨的測試用例中測試所有配置設定僅證明了一件事:“ 您知道如何複製和貼上。”
6.清晰一致地命名您的單元測試
好吧,這可能是最重要的一點,要記住並保持關注。 您必須根據測試案例的實際用途和測試來命名它們。 使用類名和方法名作為測試用例名稱的測試用例命名約定從來都不是一個好主意。 每次更改方法名稱或類名稱時,您最終也會更新很多測試用例。
但是,如果您的測試用例名稱是邏輯的,即基於操作,則幾乎不需要修改,因為大多數應用邏輯將保持不變。
例如。 測試用例名稱應類似於:
1) TestCreateEmployee_NullId_ShouldThrowException
2) TestCreateEmployee_NegativeId_ShouldThrowException
3) TestCreateEmployee_DuplicateId_ShouldThrowException
4) TestCreateEmployee_ValidId_ShouldPass
7.首先對依賴關係最少的方法編寫測試,然後逐步進行
該原則表明,如果要測試Employee
模組,則應該首先測試Employee
模組的建立,因為它對外部測試用例的依賴項最小。 一旦完成,就開始編寫Employee
修改的測試用例,因為它們需要一些僱員在資料庫中。
要在資料庫中擁有一些員工,您的建立員工測試用例必須先透過,然後才能繼續。 這樣,如果員工建立邏輯中存在一些錯誤,則可以更早地發現它。
8.所有方法,無論是否可見,都應進行適當的單元測試
好吧,這確實是有爭議的。 您需要查詢程式碼中最關鍵的部分,並且應該對其進行測試,而不必擔心它們是否是私有的。 這些方法可以具有從一到兩個類呼叫的某些關鍵演算法,但是它們起著重要的作用。 您想確保它們按預期工作。
9.針對每種單元測試方法,精確執行一個斷言
即使這不是經驗法則,您也應該嘗試在一個測試用例中僅測試一件事。 不要在單個測試用例中使用斷言來測試多個事物。 這樣,如果某個測試用例失敗,則可以確切地知道出了什麼問題。
10.建立針對異常的單元測試
如果某些測試用例希望從應用中丟擲異常,請使用“Expected
”屬性。 嘗試避免在catch
塊中捕獲異常,並使用fail
或asset
方法結束測試。
@Test(expected=SomeDomainSpecificException.SubException.class)
11.使用最合適的斷言方法
每個測試用例都可以使用許多斷言方法。 運用最適當的理由和思想。 他們在那裡是有目的的。 尊敬他們。
12.以正確的順序放置斷言引數
斷言方法通常採用兩個引數。 一個是期望值,第二個是原始值。 根據需要依次傳遞它們。 如果出現問題,這將有助於正確的訊息解析。
13.確保測試程式碼與生產程式碼分開
在您的構建指令碼中,確保測試程式碼未與實際原始碼一起部署。 這浪費了資源。
14.不要在單元測試中列印任何內容
如果您正確地遵循了所有準則,那麼您將不需要在測試用例中新增任何列印語句。 如果您想擁有一個,請重新訪問您的測試用例,您做錯了什麼。
15.不要在測試類中使用靜態成員。 如果您已經使用過,則針對每個測試用例重新初始化
我們已經說過,每個測試用例都應該彼此獨立,因此永遠不需要靜態資料成員。 但是,如果您在緊急情況下需要任何幫助,請記住在執行每個測試用例之前將其重新初始化為初始值。
16.不要寫自己的只能使測試失敗的catch
塊
如果測試程式碼中的任何方法引發某些異常,則不要編寫catch
塊只是為了捕獲異常並使測試用例失敗。 而是在測試用例宣告本身中使用throws Exception
語句。 我將建議使用Exception
類,並且不要使用Exception
的特定子類。 這也將增加測試範圍。
17.不要依賴間接測試
不要假定特定的測試用例也會測試另一種情況。 這增加了歧義。 而是為每種情況編寫另一個測試用例。
18.將測試用例與構建指令碼整合
最好將測試用例與構建指令碼整合在一起,以使其在生產環境中自動執行。 這提高了應用以及測試設定的可靠性。
19.不要跳過單元測試
如果某些測試用例現在無效,則將其從原始碼中刪除。 不要使用@Ignore
或svn.test.skip
來跳過它們的執行。 在原始碼中包含無效的測試用例不會幫助任何人。
20.使用 XML 格式器捕獲結果
這是感覺良好的因素。 它絕對不會帶來直接的好處,但是可以使執行單元測試變得有趣和有趣。 您可以將 JUnit 與 ant 構建指令碼整合,並生成測試用例,並使用一些顏色編碼以 XML 格式執行報告。 遵循也是一種很好的做法。
總結
毫無疑問,單元測試可以顯著提高專案質量。 我們這個行業中的許多學者聲稱,任何單元測試總比沒有好,但是我不同意:測試套件可以成為一項重要資產,但是不良套件可以成為負擔不起的同樣巨大的負擔。 這取決於這些測試的質量,這似乎取決於其開發人員對單元測試的目標和原理的理解程度。
如果您理解上述準則,並嘗試在下一組測試用例中實現其中的大多數準則,那麼您一定會感到與眾不同。
請讓我知道您的想法。
學習愉快!
2. 用 JUnit 編寫測試
在 JUnit 中,測試方法帶有@Test
註解。 為了執行該方法,JUnit 首先構造一個新的類例項,然後呼叫帶註解的方法。 測試丟擲的任何異常將由 JUnit 報告為失敗。 如果未引發任何異常,則假定測試成功。
import java.util.ArrayList;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class Example {
@BeforeClass
public static void setup() {
}
@Before
public void setupThis() {
}
@Test
public void method() {
org.junit.Assert.assertTrue(new ArrayList().isEmpty());
}
@After
public void tearThis() {
}
@AfterClass
public static void tear() {
}
}
3. 斷言
在JUnit 4中,org.junit.Assert
類提供了一系列的靜態方法,用於在單元測試中進行斷言(Assertion)。斷言是測試程式碼中的一種關鍵機制,用於驗證程式碼的行為是否符合預期。如果斷言失敗,即實際結果與預期結果不一致,那麼測試就會失敗。
以下是一些常用的Assert
方法:
-
assertEquals
: 驗證兩個值是否相等。Assert.assertEquals(expected, actual);
-
assertTrue
: 驗證一個布林表示式是否為true
。Assert.assertTrue(booleanExpression);
-
assertFalse
: 驗證一個布林表示式是否為false
。Assert.assertFalse(booleanExpression);
-
assertNotNull
: 驗證一個引用是否不是null
。Assert.assertNotNull(object);
-
assertNull
: 驗證一個引用是否是null
。Assert.assertNull(object);
-
assertSame
: 驗證兩個引用是否指向同一個物件。Assert.assertSame(expected, actual);
-
assertNotSame
: 驗證兩個引用是否指向不同的物件。Assert.assertNotSame(val1, val2);
-
assertArrayEquals
: 驗證兩個陣列是否相等。Assert.assertArrayEquals(expectedArray, actualArray);
-
fail
: 手動標記測試為失敗。Assert.fail("Test failed with a message");
下面是一個使用Assert
方法的JUnit測試示例:
import org.junit.Test;
import static org.junit.Assert.*;
public class ExampleTest {
@Test
public void testAddition() {
int expected = 5;
int actual = 2 + 3;
assertEquals("Addition method failed", expected, actual);
assertTrue("2 + 3 should be greater than 4", actual > 4);
assertNotNull("The result should not be null", actual);
}
}
在這個例子中,我們測試了一個簡單的加法操作,使用了assertEquals
來驗證實際結果是否與預期結果相等,使用了assertTrue
來驗證一個布林表示式,以及assertNotNull
來確保結果不為null
。
斷言方法通常接受兩個引數:一個是預期值,另一個是實際值。有些斷言方法還可以接受一個字串訊息,該訊息在斷言失敗時顯示,以幫助快速定位問題。
使用斷言是編寫有效單元測試的關鍵部分,它確保了測試的準確性和可維護性。
2. 註解
1. @Test
- 表示該方法是一個測試方法。JUnit將會執行標記了此註解的方法。
2. @Before
- 表示在每個測試方法之前執行的方法,通常用於測試環境的初始化。
3. @After
- 表示在每個測試方法之後執行的方法,通常用於清理測試環境。
4. @BeforeClass
`
- 表示在所有測試方法之前僅執行一次的方法,必須是靜態方法。通常用於一次性的全域性設定。
5. @AfterClass
- 表示在所有測試方法之後僅執行一次的方法,必須是靜態方法。通常用於一次性的全域性清理。
6. @Ignore
- 表示暫時忽略該測試方法,不執行它。
7. @RunWith
- 用於指定一個自定義的測試執行器。例如,可以使用`@RunWith(Parameterized.class)`來進行引數化測試。
8. @Test(expected = Exception.class)
- 表示預期在執行該測試方法時丟擲指定型別的異常。如果沒有丟擲異常或丟擲不同型別的異常,測試將失敗。
9. @Test(timeout = 1000)
- 表示測試方法應該在指定的毫秒數內完成。如果超出指定時間,測試將失敗。
3. 測試類監聽器
在JUnit 4中,RunListener
是一個介面,它允許開發者介入測試執行的生命週期中的各個點。透過實現 RunListener
介面,可以自定義測試執行過程中的行為,例如測試開始、結束、發生錯誤或失敗時的處理等。
以下是 RunListener
中一些關鍵的方法:
testRunStarted
: 當整個測試執行開始時呼叫。testRunFinished
: 當整個測試執行結束時呼叫。testStarted
: 當單個測試方法開始執行時呼叫。testFinished
: 當單個測試方法執行結束時呼叫。testFailure
: 當測試失敗時呼叫。testAssumptionFailure
: 當測試因為一個假設(assumption)失敗而不是失敗(failure)時呼叫。testIgnored
: 當測試被忽略時呼叫。
要使用 RunListener
,你需要實現該介面,並重寫你感興趣的方法。然後,你需要告訴 JUnit 你的 RunListener
實現應該被使用。這可以透過幾種方式完成:
-
透過程式化註冊:在你的測試程式碼中,使用
Request.runListeners
方法註冊RunListener
。 -
透過 JUnit 的
@Rule
機制:建立一個實現TestRule
介面的類,該類在其apply
方法中註冊RunListener
。 -
作為 JUnit 的啟動引數:在命令列或 IDE 配置中指定你的
RunListener
實現。
下面是一個簡單的 RunListener
實現示例,它在控制檯輸出每個測試的開始和結束:
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.RunListener;
public class SimpleRunListener extends RunListener {
@Override
public void testStarted(Description description) {
System.out.println("Test started: " + description.getMethodName());
}
@Override
public void testFinished(Description description) {
System.out.println("Test finished: " + description.getMethodName());
}
}
要在測試套件中使用這個監聽器,你可以在測試類上使用 @Rule
註解一個欄位:
import org.junit.Rule;
import org.junit.Test;
public class MyTest {
@Rule
public SimpleRunListener simpleRunListener = new SimpleRunListener();
@Test
public void test1() {
// Your test code here
}
@Test
public void test2() {
// Your test code here
}
}
這樣,每當測試執行時,SimpleRunListener
都會輸出測試的開始和結束資訊。
4. 有序執行測試案例
編寫 JUnit 有序測試案例被認為是不良做法。 但是,如果仍然遇到測試用例排序是唯一出路的情況,則可以使用MethodSorters
類
在JUnit 4中,@FixMethodOrder
是一個註解,它確保測試方法按照指定的順序執行。預設情況下,JUnit 可能會隨機化測試方法的執行順序,以避免依賴測試執行順序的問題。但是,有些情況下,你可能需要確保測試按照特定的順序執行,比如當測試方法之間存在依賴關係時。
要使用 @FixMethodOrder
,首先需要匯入相應的包:
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
然後,你可以在測試類上使用 @FixMethodOrder
註解,並指定排序策略。JUnit 4 提供了 MethodSorters
工具類,其中包含了幾種預定義的排序方式:
MethodSorters.NAME_ASCENDING
:按照方法名稱的字典順序升序排列。MethodSorters.NAME_DESCENDING
:按照方法名稱的字典順序降序排列。- 等等。
下面是一個使用 @FixMethodOrder
的例子:
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class MyTestClass {
@Test
public void test1() {
// 測試邏輯
}
@Test
public void test2() {
// 測試邏輯
}
@Test
public void test3() {
// 測試邏輯
}
}
在這個例子中,即使測試方法的名稱可能不會決定它們的執行順序,@FixMethodOrder
註解確保了 test1
、test2
和 test3
將按照它們在類中出現的順序執行。
請注意,依賴測試執行順序的做法通常不是一個好習慣,因為它可能導致測試變得脆弱和難以維護。在大多數情況下,你應該儘量編寫獨立且不依賴於其他測試的測試用例。
5. 建立臨時資料夾
在JUnit 4中,TemporaryFolder
是一個JUnit的Rule
,它提供了建立和刪除臨時資料夾和檔案的便捷方式,這在需要測試檔案和資料夾操作的單元測試中非常有用。TemporaryFolder
可以確保測試執行結束後,所有的臨時檔案和資料夾都被清理乾淨,避免測試之間的相互干擾。
以下是如何在JUnit 4測試中使用TemporaryFolder
的基本步驟:
-
首先,確保你的專案中包含了JUnit依賴。如果你使用的是Maven或Gradle,確保你的
pom.xml
或build.gradle
檔案中包含了JUnit的依賴項。 -
匯入
TemporaryFolder
類和JUnit的Rule
註解:
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
- 在測試類中,使用
@Rule
註解宣告一個TemporaryFolder
的例項:
public class TemporaryFolderExampleTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
}
- 在測試方法中,使用
TemporaryFolder
的例項來建立臨時檔案和資料夾:
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
public class TemporaryFolderExampleTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void testUsingTemporaryFolder() throws IOException {
// 建立一個臨時資料夾
File tempFolder = folder.newFolder();
System.out.println("Temporary folder created at: " + tempFolder.getAbsolutePath());
// 在臨時資料夾中建立一個檔案
File tempFile = folder.newFile("test.txt");
System.out.println("Temporary file created at: " + tempFile.getAbsolutePath());
// 執行測試邏輯,例如寫入檔案等
// 測試結束後,TemporaryFolder Rule會自動刪除這些臨時檔案和資料夾
}
}
在上述示例中,TemporaryFolder
會在測試方法執行前建立一個新的臨時資料夾和檔案。測試完成後,不管測試是否透過,TemporaryFolder
都會刪除這些臨時檔案和資料夾,確保測試環境的整潔。
使用TemporaryFolder
可以幫助你編寫更乾淨、更可重複的測試,特別是在測試需要檔案系統操作的程式碼時。