關於測試覆蓋率

關於測試覆蓋率

您還記得大多數開發人員踏上代碼質量潮流以前的狀況嗎?在那些日子裏,熟練地放置main() 方法被認爲既敏捷又足以進行測試。從那時起,咱們已經走了很長一段路。首先,我很是感謝自動化測試現已成爲以質量爲中心的代碼開發的重要方面。這不是我要感謝的所有。Java開發人員擁有大量工具,可經過代碼指標,靜態分析等來衡量代碼質量,咱們甚至設法將重構歸爲一組便捷的模式!java

確保您的代碼質量

全部這些新工具使確保代碼質量比以往更加容易,可是您必須知道如何使用它們。在本系列文章中,我將重點介紹確保代碼質量的有時有些難以想象的細節。除了使您熟悉可用於代碼質量保證的各類工具和技術以外,我還將向您展現如何解決如下問題:編程

  • 定義並有效衡量對代碼質量影響最大的方面。
  • 設定質量保證目標並相應地計劃您的開發工做。
  • 肯定哪些代碼質量工具和技術真正知足您的需求。
  • 實施最佳實踐(並淘汰不良實踐),以便儘早確保代碼質量,而且一般成爲開發實踐中不費力且有效的方面。
  • 我將從這個月開始,看看Java開發人員的質量保證工具包中最流行,最簡單的功能之一:測試覆蓋率測量。

小心被忽悠

使用測試覆蓋率工具沒有任何欺騙的可能。它們是單元測試範例的一個很好的補充。重要的你在獲取到這些信息的時候,如何綜合考量並加以推廣,這是一些開發團隊犯下的第一個錯誤。segmentfault

高覆蓋率僅意味着要執行大量代碼。高覆蓋率並不意味着代碼能夠很好地執行。若是您專一於代碼質量,則須要準確瞭解測試覆蓋率工具的工做原理以及它們如何工做;而後您將知道如何使用這些工具來獲取有價值的信息,而不只僅是像許多開發人員同樣,爲實現高覆蓋率目標而寫了大量的測試代碼。瀏覽器

測試覆蓋率測量

測試覆蓋率工具一般很容易添加到已創建的單元測試過程當中,而且結果能夠放心。只需下載一個可用工具,略微修改Ant或Maven構建腳本,您和您的同事就能夠圍繞測試質量提出一種新的報告:「測試覆蓋率報告」。當報告顯示出驚人的高覆蓋率時,這多是一個很大的安慰;當您相信至少一部分代碼能夠證實是「無錯誤的」時,就容易放鬆。可是這樣作將是一個錯誤。安全

覆蓋率度量有不一樣的類型,可是大多數工具都關注行覆蓋率,也稱爲語句覆蓋率。另外,某些工具報告分支機構覆蓋率。經過使用測試工具來運行代碼庫並捕獲與在整個測試過程的生命週期中「被執行」的代碼相對應的數據,能夠得到測試覆蓋率的測量結果。而後將數據合成以生成覆蓋率報告。在Java經常使用庫中,測試工具一般是JUnit,覆蓋工具一般是諸如Cobertura,Emma或Clover之類的工具。框架

行覆蓋率只是代表已執行了特定的代碼行。若是某個方法長10行,而且在測試運行中使用了8行,則該方法的行覆蓋率爲80%。該過程也適用於彙總級別:若是一個類有100行,其中有45行被觸摸,則該類的行覆蓋率爲45%。一樣,若是一個代碼庫包含10,000條非註釋行代碼,而且其中3500條是在特定測試運行中執行的,則該代碼庫的行覆蓋率爲35%。函數

報告分支覆蓋率的工具會嘗試測量決策點的覆蓋率,例如包含邏輯條件代碼塊 。就像行覆蓋率同樣,若是特定方法中有兩個分支而且都經過測試覆蓋,那麼您能夠說該方法具備100%的分支覆蓋率。工具

問題是,這些測量有用嗎?顯然,全部這些信息都很容易得到,可是要由您來辨別如何綜合這些信息得出合適的結論。一些例子闡明瞭個人觀點。性能

  • 實際的代碼覆蓋率

我在清單1中建立了一個簡單的類,以體現類層次結構的概念。給定的類能夠具備一系列超類-例如 Vector,其父級爲AbstractList,其父級爲AbstractCollection,其父級爲 Object:單元測試

  • 清單1.表明類層次結構的類:
package com.vanward.adana.hierarchy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Hierarchy {

    private Collection classes;

    private Class baseClass;

    public Hierarchy() {
        super();
        this.classes = new ArrayList();
    }

    public void addClass(final Class clzz) {
        this.classes.add(clzz);
    }
    /**
     * @return an array of class names as Strings
     */
    public String[] getHierarchyClassNames() {
        final String[] names = new String[this.classes.size()];
        int x = 0;
        for (Iterator iter = this.classes.iterator(); iter.hasNext();) {
            Class clzz = (Class) iter.next();
            names[x++] = clzz.getName();
        }
        return names;
    }

    public Class getBaseClass() {
        return baseClass;
    }

    public void setBaseClass(final Class baseClass) {
        this.baseClass = baseClass;
    }
}

如您所見,清單1的Hierarchy類包含一個 baseClass實例及其超類的集合。在 HierarchyBuilder清單2中建立 Hierarchy經過兩個重載類static 冠以方法buildHierarchy()。

  • 清單2.類層次結構構建器:
package com.vanward.adana.hierarchy;

public class HierarchyBuilder {

    private HierarchyBuilder() {
        super();
    }

    public static Hierarchy buildHierarchy(final String clzzName)
            throws ClassNotFoundException {
        final Class clzz = Class.forName(clzzName, false,
                HierarchyBuilder.class.getClassLoader());
        return buildHierarchy(clzz);
    }

    public static Hierarchy buildHierarchy(Class clzz) {
        if (clzz == null) {
            throw new RuntimeException("Class parameter can not be null");
        }

        final Hierarchy hier = new Hierarchy();
        hier.setBaseClass(clzz);

        final Class superclass = clzz.getSuperclass();

        if (superclass !=
                null && superclass.getName().equals("java.lang.Object")) {
            return hier;
        } else {
            while ((clzz.getSuperclass() != null) &&
                    (!clzz.getSuperclass().getName().equals("java.lang.Object"))) {
                clzz = clzz.getSuperclass();
                hier.addClass(clzz);
            }
            return hier;
        }
    }
}

測試時間到了!

若是沒有測試用例,關於測試覆蓋率的文章將會是什麼?在清單3中,我定義了一個簡單的JUnit測試類,其中包含三個測試用例,它們試圖同時使用 Hierarchy和HierarchyBuilder類:

  • 清單3.測試HierarchyBuilder:
package test.com.vanward.adana.hierarchy;

import com.vanward.adana.hierarchy.Hierarchy;
import com.vanward.adana.hierarchy.HierarchyBuilder;
import junit.framework.TestCase;

public class HierarchyBuilderTest extends TestCase {

    public void testBuildHierarchyValueNotNull() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertNotNull("object was null", hier);
    }

    public void testBuildHierarchyName() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertEquals("should be junit.framework.Assert",
                "junit.framework.Assert",
                hier.getHierarchyClassNames()[1]);
    }

    public void testBuildHierarchyNameAgain() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertEquals("should be junit.framework.TestCase",
                "junit.framework.TestCase",
                hier.getHierarchyClassNames()[0]);
    }

}

由於我是一名「認真」的測試人員,因此我天然但願進行一些覆蓋率測試。在Java開發人員可用的代碼覆蓋工具中,我傾向於使用Cobertura,由於我喜歡它的友好報告。一樣,Cobertura是一個開源項目,它是開拓性的JCoverage項目的分支。

Cobertura報告

運行像Cobertura這樣的工具就像運行JUnit測試同樣簡單,只有中間步驟,使用專門的邏輯對被測代http://pic.automancloud.com碼...(這所有經過工具的Ant任務或Maven的目標進行處理)。

正如你在圖中看到,用於覆蓋報告 HierarchyBuilder說明的代碼幾行不執行。實際上,Cobertura報告顯示其 HierarchyBuilder線路覆蓋率爲59%,分支覆蓋率爲75%。

覆蓋率報告截圖

所以,覆蓋率測試的第一槍未能測試不少東西。首先,根本沒有測試buildHierarchy()以String類型做爲參數的方法 。其次,另buildHierarchy()一種方法中的兩個條件均未執行。有趣的是,這是第二個未執行的 if條件代碼塊。

我如今不擔憂,由於我要作的就是添加更多測試用例。一旦到達這些使人關注的領域,我應該會很好。在這裏注意個人邏輯:我使用覆蓋率報告瞭解未測試的內容。如今,我能夠選擇使用此數據來加強測試或繼續前進。在這種狀況下,我將加強測試,由於我發現了一些重要的事情。

Cobertura:第2輪

清單4是更新後的JUnit測試用例,其中添加了一些其餘測試用例,以嘗試全面行使HierarchyBuilder:

  • 清單4.更新的JUnit測試用例:
package test.com.vanward.adana.hierarchy;

import com.vanward.adana.hierarchy.Hierarchy;
import com.vanward.adana.hierarchy.HierarchyBuilder;
import junit.framework.TestCase;

public class HierarchyBuilderTest extends TestCase {

    public void testBuildHierarchyValueNotNull() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertNotNull("object was null", hier);
    }

    public void testBuildHierarchyName() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertEquals("should be junit.framework.Assert",
                "junit.framework.Assert",
                hier.getHierarchyClassNames()[1]);
    }

    public void testBuildHierarchyNameAgain() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertEquals("should be junit.framework.TestCase",
                "junit.framework.TestCase",
                hier.getHierarchyClassNames()[0]);
    }

    public void testBuildHierarchySize() {
        Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
        assertEquals("should be 2", 2, hier.getHierarchyClassNames().length);
    }

    public void testBuildHierarchyStrNotNull() throws Exception {
        Hierarchy hier =
                HierarchyBuilder.
                        buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
        assertNotNull("object was null", hier);
    }

    public void testBuildHierarchyStrName() throws Exception {
        Hierarchy hier =
                HierarchyBuilder.
                        buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
        assertEquals("should be junit.framework.Assert",
                "junit.framework.Assert",
                hier.getHierarchyClassNames()[1]);
    }

    public void testBuildHierarchyStrNameAgain() throws Exception {
        Hierarchy hier =
                HierarchyBuilder.
                        buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
        assertEquals("should be junit.framework.TestCase",
                "junit.framework.TestCase",
                hier.getHierarchyClassNames()[0]);
    }

    public void testBuildHierarchyStrSize() throws Exception {
        Hierarchy hier =
                HierarchyBuilder.
                        buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
        assertEquals("should be 2", 2, hier.getHierarchyClassNames().length);
    }

    public void testBuildHierarchyWithNull() {
        try {
            Class clzz = null;
            HierarchyBuilder.buildHierarchy(clzz);
            fail("RuntimeException not thrown");
        } catch (RuntimeException e) {
        }
    }
}

當我使用新的測試用例再次運行測試覆蓋率過程時,我獲得了更加完整的報告,如圖所示。我如今介紹了未經測試的buildHierarchy()方法以及if在另buildHierarchy()一種方法中都遇到了問題 。 HierarchyBuilder的構造函數是private,因此我沒法經過個人測試類對其進行測試(也不關心);所以,個人線路覆蓋率仍然徘徊在88%。

覆蓋率測試第二輪

條件判斷的錯誤

如您所見,使用代碼覆蓋率工具能夠發現沒有相應測試用例的重要代碼。重要的是在查看報告(尤爲是具備較高價值的報告)時要格外當心,由於它們可能掩蓋錯誤的微妙之處很難讓人發現。讓咱們看幾個隱藏在高覆蓋率背後的代碼問題示例。

  • 清單5.您看到下面的缺陷了嗎?
package com.vanward.coverage.example01;

public class PathCoverage {

  public String pathExample(boolean condition){
    String value = null;
    if(condition){
      value = " " + condition + " ";
    }
    return value.trim();
  }
}

清單5中有一個陰險的缺陷-您看到了嗎?若是沒有,請不用擔憂:我將編寫一個測試用例來練習該 pathExample()方法,並確保它在清單6中正確運行:

  • 清單6.搶救JUnit!
package test.com.vanward.coverage.example01;

import junit.framework.TestCase;
import com.vanward.coverage.example01.PathCoverage;

public class PathCoverageTest extends TestCase {

  public final void testPathExample() {
    PathCoverage clzzUnderTst = new PathCoverage();
    String value = clzzUnderTst.pathExample(true);
    assertEquals("should be true", "true", value);
  }
}

個人測試用例運行無懈可擊,而我方便的代碼覆蓋率報告(如圖所示)使我看起來像超級明星,具備100%的測試覆蓋率!

我想是時候該去喝水了,我是否懷疑該代碼中存在缺陷?清單5的仔細檢查顯示,第13行確實會拋出NullPointerException if conditionis false。是的,這裏發生了什麼?

事實證實,線路覆蓋率並非測試有效性的很好指標。

質量測試

我再說一遍:您能夠(而且應該)在測試過程當中使用測試覆蓋率工具,可是不要被覆蓋率報告所迷惑。關於覆蓋率報告的主要理解是,它們最好用於公開未經充分測試的代碼。查看覆蓋率報告時,請找出較低的值,並瞭解爲何未對特定代碼進行完整測試。知道了這一點,開發人員,經理和質量檢查專業人員可使用他們真正認爲有用的測試覆蓋率工具。即針對三種常見狀況:

  • 估計修改現有代碼的時間
  • 評估代碼質量
  • 評估功能測試

既然我已經創建了一些測試覆蓋率報告可使您避免誤入歧途的方法。下面請考慮使用這些最佳實踐以使您受益。

1.估計修改現有代碼的時間

針對代碼編寫測試用例天然會提升開發團隊的集體信心。通過測試的代碼比沒有相應測試用例的代碼更易於重構,維護和加強。測試用例也能夠做爲熟練的文檔,由於它們隱式演示了被測代碼的工做方式。並且,若是測試中的代碼發生更改,則測試用例一般會並行更改,這與靜態代碼文檔(例如註釋和Javadocs)不一樣。

在另外一方面,沒有相應測試的代碼可能更難以理解,而且更難安全修改。所以,瞭解代碼是否已通過測試,並查看實際的測試覆蓋率數字,可使開發人員和管理人員更準確地預測修改現有代碼所需的時間。

2.評估代碼質量

開發人員測試下降了代碼缺陷的風險,所以許多開發團隊如今要求將單元測試與新開發或修改的代碼一塊兒編寫。可是,如上文所示,單元測試並不老是與編碼並行進行,這可能致使較低質量的代碼。

監視覆蓋率報告可幫助開發團隊快速發現正在增加的代碼,而無需進行相應的測試。例如,在本週初運行覆蓋報告,則代表該項目中的關鍵軟件包的覆蓋率爲70%。若是本週晚些時候該軟件包的覆蓋率降至60%,則能夠推斷出:

該軟件包的代碼行有所增長,可是沒有爲新代碼編寫相應的測試(或者新添加的測試不能有效地覆蓋新代碼)、測試用例被刪除、這兩件事同時發生。
高明之處在於可以觀察趨勢。按期查看報告能夠更輕鬆地設置目標(例如得到覆蓋率,維護測試用例與代碼比率行等),而後監視其進度。若是您碰巧發現一般沒有編寫測試,則能夠採起主動措施,例如設置開發人員進行培訓,指導或夥伴編程。當客戶發現及其隱藏的缺陷(可能在幾個月前經過簡單的測試暴露出來)時,或在管理層發現單元測試未免時,不可避免的意外(和憤怒)比之,明智的響應要好得多。

使用覆蓋率報告來確保正確的測試是一個好習慣。訣竅是要有紀律地作到這一點。例如,做爲可持續集成過程的一部分,請嘗試天天生成和查看覆蓋率報告。

3.評估功能測試

鑑於代碼覆蓋率報告在不進行適當測試的狀況下最能說明代碼部分,所以質量保證人員可使用此數據來評估與功能測試有關的領域。

一樣,知識就是力量。經過與軟件生命週期中的其餘利益相關者(例如質量保證)進行仔細協調,您可使用覆蓋率報告提供的看法來促進風險緩解。

測試取得回報的地方

測試覆蓋率測量工具是對單元測試範例的絕佳補充。覆蓋率測量是有效的過程又提供了深度和精確度。可是,您應該謹慎地查看代碼覆蓋率報告。高覆蓋率自己並不能確保代碼的質量。覆蓋率很高的代碼不必定沒有缺陷,儘管包含缺陷的可能性確定較小。

測試覆蓋率度量的技巧是使用覆蓋率報告在微觀級別和宏觀級別公開未經測試的代碼。經過從頂層分析代碼庫以及分析各個類的覆蓋範圍,能夠促進更深刻的覆蓋範圍測試。集成了該原理後,您和您的組織就可使用覆蓋率測量工具,它們能夠真正地發揮做用,例如估算項目所需的時間,持續監控代碼質量並促進QA協做。


  • 鄭重聲明:文章首發於公衆號「FunTester」,禁止第三方(騰訊雲除外)轉載、發表。

技術類文章精選

非技術文章精選

相關文章
相關標籤/搜索