關於測試覆蓋率


關於測試覆蓋率

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

確保您的代碼質量

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

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

當心被忽悠

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

高覆蓋率僅意味着要執行大量代碼。高覆蓋率並不意味着代碼可以很好地執行。如果您專注於代碼質量,則需要准確了解測試覆蓋率工具的工作原理以及它們如何工作;然后您將知道如何使用這些工具來獲取有價值的信息,而不僅僅是像許多開發人員一樣,為實現高覆蓋率目標而寫了大量的測試代碼。

測試覆蓋率測量

測試覆蓋率工具通常很容易添加到已建立的單元測試過程中,並且結果可以放心。只需下載一個可用工具,略微修改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”,禁止第三方(騰訊雲除外)轉載、發表。

技術類文章精選

非技術文章精選


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM