使用 JUnit4編寫單元測試

原文連接: blog.yoryor.top/2017-11-02/…html

  • 主要內容: 本文從 What, Why, When, How, Deep 幾個方面來介紹單元測試相關的基礎知識。
  • 適合閱讀: 對單元測試不瞭解或者只知其一;不知其二的程序員
  • 須要技能: 瞭解 Java 編程語言,可以使用 IDEA 等。

What

單元測試(unit test)並不一樣於普通的端到端測試,這是一項須要程序員經過實際編碼來完成,而且與程序的設計和調試緊密相關的任務。單元的大小,並無嚴格規定,可是遵守着軟件設計中」SRP」(Single Responsibility Principle)原則,在 Java 中一般以類做爲一個基本的測試單元,以此編寫單元測試來對功能或者說行爲進行監視和檢測。
JUnit 是 Java 編程語言中比較流行的一款單元測試框架,可以知足平時開發中大部分的需求。java

Why

編寫單元測試的主要目的是爲了檢視代碼的行爲是否符合預期,而且這種檢查一般是成本很低的,開發人員能夠方便的在 IDE 或本地開發環境中及時的發現問題,從而提升開發效率。git

When

編寫合理的單元測試能夠輕鬆應付如下的幾個場景程序員

  • 檢查新添加的代碼功能是否符合預期
  • 檢查新添加的代碼是否兼容舊版本
  • 第三方庫代碼的運行是否符合預期
  • 瞭解當前代碼的行爲

How

在 IDEA 中使用單元測試是很方便的,能夠參考官方的教程建立單元測試
接下來使用的示例代碼模擬實現了一個功能,根據一個程序員的技能和等級,爲一個程序員打分。詳細的代碼能夠到 github 上查看 code-example, 推薦根據提交歷史來進行查看。具體的環境以下:github

  • JDK:1.8+
  • IDEA:2017.2
  • JUnit: 4.12

註解

Junit4 開始使用的註釋提升了單元測試的編寫效率,在引入 Junit4依賴後,在須要進行測試的方法上添加一個@Test 註釋便可。其餘經常使用的註釋還有@Before、@After、@Rule 等。在後面遇到的時候在詳述編程

命名

良好的測試方法命名能起到見名知義的效果。理想狀況下,命名中須要包含測試的方法,條件以及指望的返回結果。能夠提煉成如下的格式
·callSomeMethodReturnSomeResultWhenSomeConditions
或者其餘的相似的變體。好比given-then-when等等框架

方法體

前面的命名中提到的三個信息也正是組織單元測試的基本依據。即編程語言

  1. 準備條件 (arrange)
  2. 執行目標方法 (act)
  3. 檢驗結果 (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 類型)
    因爲代碼很簡單,因此將執行和驗證的兩個階段合併到一塊兒了。單元測試

咱們主要看一下不一樣風格的斷言:

  1. classic 風格,即驗證某項條件爲真。在這個方法中即須要驗證執行方法的結果 skills.getResult與咱們指望的結果是否相等。也就是skills.getResult() == 0
  2. matchers 風格,經過語義化的代碼來驗證結果是否匹配。整個代碼不須要太多的邏輯思考,更貼近咱們的閱讀習慣 — skills.getResult() is equal to 0d
    在 Junit 中,Assert 類中提供了基礎的斷言 API。經過 assertThat(actualResult, matchers) 方法能夠利用 Hamcrest 開發的大量 Matcher 爲咱們的單元測試提供更好的語義化支持。
    在此,我不強烈推薦其中任何一種,可是須要注意的是一個項目中的斷言風格應該儘可能保持一直。

測試異常

在編寫單元測試時,若是測試結果不符合預期,JUnit 會報告一次failure;若是單元測試程序運行過程當中若是出現了異常,則會報告一次error。因爲單元測試的隔離性,咱們一般將關注點集中在須要測試的功能上,而不須要浪費時間在異常處理上,所以一些受檢查異常直接在方法簽名上拋出便可。
有些時候咱們可能會須要驗證異常拋出的正確性,以此來保證客戶端使用的正確性和可靠性,有三種形式能夠來驗證異常行爲是否合理。

  • 在 @Test 註解中增長 expected 屬性。以下代碼若是去掉 expected 屬性,執行單元測試則會因爲運行時異常而報告一次 error
    @Test(expected = IllegalArgumentException.class)
          public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
              // arrange
              SkillGraph skills = new SkillGraph();
              // act
              skills.add(null);
              // assert
          }複製代碼
  • 使用 try-catch 方法來驗證。以下,雖然能夠實現,可是不夠優雅。
    @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
          }複製代碼

Deep

優化重構測試代碼

在編寫單元測試時,分支條件邊界條件是須要重點關注的。這也會致使一個類的單元測試每每須要編寫不少單元測試。所以有必要經過優化重構來保持代碼的整潔和良好的可維護性和擴展性。
在單元測試中,咱們在 arrange 階段每每會執行一些對象初始化等一些初始化操做,這個過程一般是重複的。能夠經過@Before 註解一個初始化方法,這樣在每一個單元測試以前都會執行這段代碼了。相似的還有@After註解,只不過不太經常使用。
其實以前提到的@Rule註解實現的就是相似@Before@After的功能,在執行一個單元測試方法的先後執行一些方法,只不過這些方法都是有具體的目標,經過規則這個語義實現。

注: JUnit 中每一個單元測試都擁有獨立的上下文環境,執行每一個測試方法時都會單獨生成一個新的實例,所以沒法保證單元測試用例的執行順序,致使咱們在單元測試中不能依賴其餘的測試結果,固然這種需求自己就是「anti pattern」的。

JUnit 運行

一個典型的單元測試用例的執行以下

  1. 建立單元測試類實例
  2. 調用@Before 方法
  3. 執行某一個@Test註釋的方法
  4. 調用@After 方法
  5. 建立新的單元測試類實例
  6. 調用@Before 方法
  7. 執行另外某一個@Test註釋的方法
  8. 調用@After 方法
    … …

邊界條件

編寫單元測試最主要、最直接的關注點就是方法執行的正確性。其次須要關注數據的邊界條件,即在某些極端的或者不正確的條件下,程序可否正常的運行或者合理的運行,以此來保證系統的健壯性。常見的關注點有如下幾個

  1. 不合實際的值(例:表示人類年齡的字段輸入180或者負數)
  2. 不符合格式的值 (例:郵件,手機等)
  3. 算數溢出
  4. 空值,Null,空集合等
  5. 不合理的重複值
  6. 沒有合理排序的值
  7. 事件發生的順序異常(例: 在建立用戶以前就進行信息編輯等)

小結

編寫單元測試算是一項內功修煉,也是一項煩瑣的工做。曾國藩曾說過,「成大事者,必能耐煩」。若是單純爲了提升代碼覆蓋率,確實很煩;若是爲了改進代碼結構防患 Bug 於未然,就會發現單元測試是如此有用。

相關文章
相關標籤/搜索