什麼是程式碼覆蓋率

爱学习的饲养员發表於2024-09-05

不知道寫過介面自動化 case 的朋友們,有沒有思考過一個問題。假如我寫了很多介面自動化 case,已經把被測系統的所有介面都覆蓋到,那這是不是就說明我的自動化 case 已經全部寫完了?是不是就說明我的自動化測試已經做得非常完備了?
答案是否定的
因為我們缺少資料來衡量自動化 case 的完備程度,那該怎麼解決呢
業界一般是透過程式碼覆蓋率來輸出自動化 case 的覆蓋資料,衡量介面自動化測試的完備程度,來指導後續要增加、完善 case 的方向。另一方面,它還可以反映服務端功能測試的全面性,用來評估服務端手工測試是否全面。
除此以外,程式碼覆蓋率還可以應用於單元測試,可以拿到經過執行單元測試用例後的覆蓋率資料。

覆蓋率定義

作為一個測試人員,保證產品的軟體質量是其工作首要目標,為了這個目標,測試人員常常會透過很多手段或工具來加以保證,覆蓋率就是其中比較重要的環節,我們通常會將測試覆蓋率分為兩個部分,即需求覆蓋率和程式碼覆蓋率。

需求覆蓋:指的是測試人員對需求的瞭解程度,根據需求的可測試性來拆分成各個子需求點,來編寫相應的測試用例,最終建立一個需求和用例的對映關係,以用例的測試結果來驗證需求的實現,可以理解為黑盒覆蓋。

程式碼覆蓋:為了更加全面的覆蓋,我們可能還需要理解被測程式的邏輯,需要考慮到每個函式的輸入與輸出,邏輯分支程式碼的執行情況,這個時候我們的測試執行情況就以程式碼覆蓋率來衡量,可以理解為白盒覆蓋。例如,如果原始碼具有一個簡單的 if...else 迴圈,則如果測試程式碼可以覆蓋這兩種情況(即 if&else),則程式碼覆蓋率將為 100%。

程式碼覆蓋率,是一種透過計算測試過程中被執行的原始碼佔全部原始碼的比例,進而間接度量軟體質量的方法。它在保證測試質量的時候潛在保證實際產品的質量,可以基於此在程式中尋找沒有被測試用例測試過的地方,進一步建立新的測試用例來增加覆蓋率。常見的程式語言,如 Java,C++,Python,JavaScript,PHP 和 Go 等,都有相應的程式碼覆蓋率統計工具。

語言種類什麼是程式碼覆蓋率 覆蓋率統計工具什麼是程式碼覆蓋率
Java cobertura、jacoco
C++ ccover、Lcov
Python Coverage.py
JavaScript istanbul
PHP xdebug、phpunit、PATest、xcache、php-code-coverage
Go gocov、go test

為什麼要測量程式碼覆蓋率

我們在做單元測試或者介面自動化測試時,你是否知道你的單元測試甚至是你的功能測試實際測試程式碼的效果?是否還需要更多的測試?這些是程式碼覆蓋率可以試圖回答的問題。總之,出於以下原因我們需要測量程式碼覆蓋率:

  • 瞭解我們的測試用例對原始碼的測試效果
  • 瞭解我們是否進行了足夠的測試
  • 在軟體的整個生命週期內保持測試質量 注:程式碼覆蓋率不是靈丹妙藥,覆蓋率測量不能替代良好的程式碼審查和優秀的程式設計實踐。

Go 覆蓋率統計

Go 語言是現在網際網路大廠很常用的語言,下面就結合 go test 命令列工具,來講解如何統計單元測試或者介面自動化測試程式碼覆蓋率。
對於 go test 提供了兩種統計覆蓋率的方式,一種是直接使用 go test 命令列,另外一種則是執行插樁後的二進位制檔案。
溫馨提示:閱讀以下內容,需要掌握 Go 語言的基礎語法。

方法一:直接執行 go test 命令統計覆蓋率

1.1 建立 main_test.go 檔案

建立 main_test.go 檔案或者與你的 main 函式所在的檔名同名的 test 檔案,假如我們有以下 main.go 檔案:

// main.go檔案
package main

import (
    "github.com/labstack/echo"
    "hello-go/api"
)

func main() {
    e := echo.New()
    e.GET("/", api.HelloWorld)
    e.GET("/api1", api.Api1)
    e.GET("/api2", api.Api2)
    e.Logger.Fatal(e.Start(":8001"))
}

根據以上 main.go 檔案,建立以下 main_test.go 檔案:

// main_test.go檔案
package main

import (
   "fmt"
   "net/http"
   "os"
   "os/signal"
   "testing"
)

var exitChan chan int
func testHandler(w http.ResponseWriter, req *http.Request) {
   exitChan <- 666
}

func startServer() {

   go main()     // 呼叫main.go檔案中的main方法,啟動服務

}

func TestExternal(t *testing.T) {
   // start server need be tested in separate go thread
   go startServer()

   // go test starts a dummy http server, which is used to
   // end the current go test gracefully when it's accessed.

   http.HandleFunc("/", testHandler)
   go http.ListenAndServe(":9999", nil)     
   // go test只有在服務程序正確退出的情況下才會生成覆蓋率檔案,因此在這裡註冊9999埠,來監聽停止服務的指令,
   // 當完成測試後,向9999埠觸發請求,服務停止,生成覆蓋率檔案

   exitChan = make(chan int)

   sigChann := make(chan os.Signal)
   signal.Notify(sigChann, os.Interrupt)

   select {
   case sig := <-sigChann:
      fmt.Printf("exit as received signal: %v\n", sig)
   case val := <-exitChan:
      fmt.Printf("exit as received http request: %v\n", val)
   }
}

1.2 執行 go test 命令

進入 main_test.go 所在目錄,一般在程式碼根目錄,執行 go test 命令。讓 go test 命令 啟動 Web 服務進行測試,產出覆蓋率檔案。

go test -coverprofile=cov.out -coverpkg ./... &
# 引數介紹
# -coverprofile 指定產出的覆蓋率檔名稱
# -coverpkg ./... 指包含該路徑下所有子包的覆蓋率結果不加此引數可能會導致覆蓋率結果中只有main檔案
# & 讓服務程序後臺執行避免啟動後馬上退出必須加!!!

執行完命令後,可以看到服務正常啟動的日誌(確保 Web 服務已經正常啟動)

1.3 執行測試用例

Web 服務啟動後,就可以開始執行你的測試用例了,例如:

curl 127.0.0.1:8001          # 用例1
curl 127.0.0.1:8001/api1     # 用例2
curl 127.0.0.1:8001/api2     # 用例3

1.4 生成程式碼覆蓋率檔案

用例執行結束,執行以下命令,傳送指令停止服務,否則無法正常生成覆蓋率檔案。

curl 127.0.0.1:9999  

此時,在程式碼根目錄將會生成 cov.out 覆蓋率檔案,cat cov.out 內容如下所示:

mode: set
hello-go/api/apis.go:9.39,11.2 1 1
hello-go/api/apis.go:13.34,15.2 1 1
hello-go/api/apis.go:17.34,19.2 1 0
hello-go/main.go:8.13,14.2 5 1

1.5 檢視覆蓋率報告

為了方便檢視和瀏覽,可將 out 檔案轉換為 html 報告進行檢視,執行如下命令:

go tool cover -html cov.out -o index.html

方法二:編譯、執行插樁二進位制檔案統計覆蓋率

除了直接執行 go test 命令,我們還可以透過執行插樁二進位制檔案來統計覆蓋率。

2.0 生成覆蓋率二進位制檔案(插樁產物)原理介紹

要執行系統進行測試,需要應用程式的編譯二進位制檔案。然後,在具有不同配置的不同環境中執行此二進位制檔案。Golang 提供了一種獨特的方法來生成覆蓋率二進位制檔案,而不是 go build 生成的預設二進位制檔案
生成的程式碼覆蓋率二進位制檔案在每一行程式碼後寫入一個唯一的計數器,並檢查在執行二進位制檔案後呼叫此計數器的次數,更多的技術細節可以在 go-cover 文件(https://go.dev/blog/cover)中找到
當執行 go test 時,覆蓋率二進位制檔案會自動生成並在之後處理。Golang 允許使用以下命令生成此覆蓋率二進位制檔案

go test -c -covermode=count -coverpkg ./...
# 引數介紹
# -c 標誌用於生成測試二進位制檔案
# -covermode=count 確保生成的二進位制檔案中包含覆蓋率計數
# -o 可以指定生成的二進位制檔案的名稱如不設該引數生成的檔案將被自動命名為packagename.test
# -coverpkg ./... 在命令末尾確保為同一路徑下的所有子包生成覆蓋率二進位制檔案但不為匯入的包生成覆蓋率二進位制檔案如果您只想覆蓋特定的包可以在這裡用逗號分隔它們

更多的引數資訊可以執行 go test -help 來檢視

2.1 建立 main_test 檔案

現在我們知道了如何生成二進位制檔案,我們必須確保二進位制檔案將按預期執行。您的程式碼需要滿足以下要求,才能按照預期生成二進位制:

  • package 中至少有一個 *_test.go 檔案,否則不會生成二進位制檔案。我建議建立 main_test.go 檔案,或者與你的 main 函式所在的檔名同名的 test 檔案。

與方法一類似,需要建立一個 main_test.go 檔案讓 go test 來插樁。

package main

import (
   "flag"
   "fmt"
   "net/http"
   "os"
   "os/signal"
   "testing"
)

var exitChan chan int
func testHandler(w http.ResponseWriter, req *http.Request) {
   exitChan <- 666
}

var systemTest *bool
func init() {
   // systemTest 引數,區分執行時是否執行系統測試
   systemTest = flag.Bool("systemTest", false, "Set to true when running system tests")
}

func TestExternal(t *testing.T) {
   if *systemTest {
      // start server need be tested in separate go thread
      // 呼叫main.go檔案中的main方法,啟動服務
      go main()

      // go test starts a dummy http server, which is used to
      // end the current go test gracefully when it's accessed.
      http.HandleFunc("/", testHandler)
      go http.ListenAndServe(":9999", nil)
      // go test只有在服務程序正確退出的情況下才會生成覆蓋率檔案,因此在這裡註冊9999埠,來監聽停止服務的指令,
      // 當完成測試後,向9999埠觸發請求,服務停止,生成覆蓋率檔案

      exitChan = make(chan int)

      sigChann := make(chan os.Signal)
      signal.Notify(sigChann, os.Interrupt)

      select {
      case sig := <-sigChann:
         fmt.Printf("exit as received signal: %v\n", sig)
      case val := <-exitChan:
         fmt.Printf("exit as received http request: %v\n", val)
      }
   }
}

該檔案定義了一個 systemTest 標誌,幷包含一個呼叫 main 函式的測試用例。
執行測試二進位制檔案開始執行測試。在我們的例子中,這意味著呼叫 TestExternal,因為這是唯一的測試。執行 TestExternal 意味著呼叫 main 函式,它將像普通二進位制檔案那樣啟動應用程式。這也就意味著執行測試產生的二進位制檔案與執行普通二進位制檔案相同,只是執行測試產生的二進位制檔案將會跟蹤覆蓋率執行,也就是我們常說的打樁。
為了防止在執行單元測試時執行此測試,新增了命令列標誌 systemTest。如果未設定,則不會呼叫 main 函式。而要執行系統測試,必須在執行測試二進位制檔案期間透過附加-systemTest 來設定標誌。

2.2 生成插樁後的覆蓋率二進位制檔案

在程式碼根目錄執行以下命令:

go test -c -covermode=count -coverpkg ./...

執行完成後將生成一個 *.test 檔案

2.3 執行二進位制檔案

要檢視二進位制檔案是否按照預期生成,可以手動執行它,檢視服務是否正常啟動。

./hello-go.test -systemTest -test.coverprofile cov.out

2.4 執行測試用例

在服務啟動後,如同方法一類似,執行你的用例,例如:

curl 127.0.0.1:8001        # 用例1
curl 127.0.0.1:8001/api1   # 用例2
curl 127.0.0.1:8001/api2   # 用例3

用例執行完畢後,執行以下命令停止服務:

curl 127.0.0.1:9999        # 用例執行結束傳送指令停止服務
# 也可以用ctrl+c結束服務但使用ctrl+c結束服務需要在編譯時將main函式中的os.Exit()更改為return
# 如果用ctrl+c結束服務那就不需要註冊9999埠了可根據業務線需求自行調整
# 但透過註冊9999埠去停服是最為保險有效的方式

2.5 生成覆蓋率檔案

服務停止後,將會生成 cov.out 覆蓋率檔案。此時,在程式碼根目錄將會生成 cov.out,cat cov.out 內容如下所示:

mode: set
hello-go/api/apis.go:9.39,11.2 1 1
hello-go/api/apis.go:13.34,15.2 1 1
hello-go/api/apis.go:17.34,19.2 1 0
hello-go/main.go:8.13,14.2 5 1

2.6 檢視覆蓋率報告

為了方便檢視和瀏覽,可將 out 檔案轉換為 html 報告進行檢視,執行如下命令:

go tool cover -html cov.out -o index.html

結束語

程式碼覆蓋率不是靈丹妙藥,它只是告訴我們有哪些程式碼沒有被測試用例 “執行到” 而已,高百分比的程式碼覆蓋率不等於高質量的有效測試。
高程式碼覆蓋率不足以衡量有效測試,具有高程式碼覆蓋率並不能充分表明我們的程式碼已經過充分測試。相反,程式碼覆蓋率更準確地給出了程式碼未被測試程度的度量。這意味著,如果我們的程式碼覆蓋率指標較低,那麼我們可以確定程式碼的重要部分沒有經過測試,然而反過來不一定正確。作為測試同學,我們還是要進行程式碼走查等測試活動,而不是一味的追求高覆蓋率。

相關文章