原文連接: blog.yoryor.top/2017-11-02/…html
- 主要內容: 本文從 What, Why, When, How, Deep 幾個方面來介紹單元測試相關的基礎知識。
- 適合閱讀: 對單元測試不瞭解或者只知其一;不知其二的程序員
- 須要技能: 瞭解 Java 編程語言,可以使用 IDEA 等。
單元測試(unit test)並不一樣於普通的端到端測試,這是一項須要程序員經過實際編碼來完成,而且與程序的設計和調試緊密相關的任務。單元的大小,並無嚴格規定,可是遵守着軟件設計中」SRP」(Single Responsibility Principle)原則,在 Java 中一般以類做爲一個基本的測試單元,以此編寫單元測試來對功能或者說行爲進行監視和檢測。
JUnit 是 Java 編程語言中比較流行的一款單元測試框架,可以知足平時開發中大部分的需求。java
編寫單元測試的主要目的是爲了檢視代碼的行爲是否符合預期,而且這種檢查一般是成本很低的,開發人員能夠方便的在 IDE 或本地開發環境中及時的發現問題,從而提升開發效率。git
編寫合理的單元測試能夠輕鬆應付如下的幾個場景程序員
在 IDEA 中使用單元測試是很方便的,能夠參考官方的教程建立單元測試
接下來使用的示例代碼模擬實現了一個功能,根據一個程序員的技能和等級,爲一個程序員打分。詳細的代碼能夠到 github 上查看 code-example, 推薦根據提交歷史來進行查看。具體的環境以下:github
Junit4 開始使用的註釋提升了單元測試的編寫效率,在引入 Junit4依賴後,在須要進行測試的方法上添加一個@Test 註釋便可。其餘經常使用的註釋還有@Before、@After、@Rule 等。在後面遇到的時候在詳述編程
良好的測試方法命名能起到見名知義的效果。理想狀況下,命名中須要包含測試的方法,條件以及指望的返回結果。能夠提煉成如下的格式
·callSomeMethodReturnSomeResultWhenSomeConditions
或者其餘的相似的變體。好比given-then-when
等等框架
前面的命名中提到的三個信息也正是組織單元測試的基本依據。即編程語言
檢驗結果 (assert)
Assert 這個單詞的直譯是斷言,意思是判斷某項條件是否爲真。在單元測試中咱們經過斷言來實現結果檢驗的語義, 以此來監視代碼的行爲。
JUnit 中可使用兩種不一樣的斷言風格— classic 和 matcher。經過代碼能夠直觀的看出兩者之間的差異。ide
public class SkillGraphTest {
@Test
public void getResultReturnZeroWhenSkillGraphEmpty() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act and assert -- classic style
assertTrue(skills.getResult() == 0);
// act and assert -- matchers style
assertThat(skills.getResult(), equalTo(0d));
}
}複製代碼
上面爲 SkillGraph 類編寫的單元測試示例中,咱們驗證的行爲是當程序員的技能圖中沒有添加任何技能時調用 getResult
方法須要返回0(double 類型)
因爲代碼很簡單,因此將執行和驗證的兩個階段合併到一塊兒了。單元測試
咱們主要看一下不一樣風格的斷言:
skills.getResult
與咱們指望的結果是否相等。也就是skills.getResult() == 0
skills.getResult() is equal to 0d
assertThat(actualResult, matchers)
方法能夠利用 Hamcrest 開發的大量 Matcher 爲咱們的單元測試提供更好的語義化支持。在編寫單元測試時,若是測試結果不符合預期,JUnit 會報告一次failure
;若是單元測試程序運行過程當中若是出現了異常,則會報告一次error
。因爲單元測試的隔離性,咱們一般將關注點集中在須要測試的功能上,而不須要浪費時間在異常處理上,所以一些受檢查異常直接在方法簽名上拋出便可。
有些時候咱們可能會須要驗證異常拋出的正確性,以此來保證客戶端使用的正確性和可靠性,有三種形式能夠來驗證異常行爲是否合理。
@Test(expected = IllegalArgumentException.class)
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act
skills.add(null);
// assert
}複製代碼
@Test
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act
try {
skills.add(null);
} catch (Exception e) {
// assert
assertThat(e, instanceOf(IllegalArgumentException.class));
assertThat(e.getMessage(), equalTo("Skill can not be null."));
}
}複製代碼
使用 @Rule 註解,利用內置的 ExpectedException 來實現。JUnit 中的 rule 規則機制實際上就是相似一種 AOP 的實現,爲單元測試提供面向切面編程的能力,詳細的內容再也不此展開了。異常的斷言要放在前面,不然代碼就沒法執行到
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
expectedException.expectMessage("Skill can not be null.");
expectedException.expect(IllegalArgumentException.class);
// arrange
SkillGraph skills = new SkillGraph();
// act
skills.add(null);
// assert
}複製代碼
在編寫單元測試時,分支條件和邊界條件是須要重點關注的。這也會致使一個類的單元測試每每須要編寫不少單元測試。所以有必要經過優化重構來保持代碼的整潔和良好的可維護性和擴展性。
在單元測試中,咱們在 arrange 階段每每會執行一些對象初始化等一些初始化操做,這個過程一般是重複的。能夠經過@Before
註解一個初始化方法,這樣在每一個單元測試以前都會執行這段代碼了。相似的還有@After
註解,只不過不太經常使用。
其實以前提到的@Rule
註解實現的就是相似@Before
和 @After
的功能,在執行一個單元測試方法的先後執行一些方法,只不過這些方法都是有具體的目標,經過規則這個語義實現。
注: JUnit 中每一個單元測試都擁有獨立的上下文環境,執行每一個測試方法時都會單獨生成一個新的實例,所以沒法保證單元測試用例的執行順序,致使咱們在單元測試中不能依賴其餘的測試結果,固然這種需求自己就是「anti pattern」的。
一個典型的單元測試用例的執行以下
@Test
註釋的方法@Test
註釋的方法編寫單元測試最主要、最直接的關注點就是方法執行的正確性。其次須要關注數據的邊界條件,即在某些極端的或者不正確的條件下,程序可否正常的運行或者合理的運行,以此來保證系統的健壯性。常見的關注點有如下幾個
編寫單元測試算是一項內功修煉,也是一項煩瑣的工做。曾國藩曾說過,「成大事者,必能耐煩」。若是單純爲了提升代碼覆蓋率,確實很煩;若是爲了改進代碼結構防患 Bug 於未然,就會發現單元測試是如此有用。