什么是測試覆蓋率
首先,該如何評審自己寫的測試用例是否滿足測試要求?是否存在漏洞與缺陷?
這就要引入一個測試覆蓋率的概念了。
測試覆蓋率
覆蓋率是用來度量測試完整性的手段,是測試效果衡量的標准,是測試技術有效性的度量:
覆蓋率 = (至少被執行一次的項目(item)數) / (項目的總數)
項目是指:語句、判定、分支、函數等等。
覆蓋率按照測試方法一般可分為三大類:
- 白盒覆蓋率:語句、判定、條件、路徑等等;
- 灰盒覆蓋率:接口相關;
- 黑盒覆蓋率:功能、性能測試;
另外還包括面向對象覆蓋率等。
注意,測試用例設計不能一味的追求覆蓋率,因為測試成本隨覆蓋率的增加而增加,要在覆蓋率和成本之間有所取舍。
白盒覆蓋率
白盒覆蓋率中使用的最常見的就是邏輯覆蓋率(Logical Coverage),也稱為代碼覆蓋率(Code Coverage)、或者稱為結構化覆蓋率(Structural Coverage)。
邏輯覆蓋率包括:
- 語句覆蓋;
- 判定覆蓋;
- 條件覆蓋;
- 判定條件覆蓋;
- 條件組合覆蓋;
- 路徑覆蓋;
語句覆蓋
語句覆蓋(Statement Coverage)的含義是,在測試時運行被測程序后,程序中被執行到的可執行語句的比率:
語句覆蓋率 = (至少被執行一次的語句數量) / (可執行的語句總數)
現在我們祭出在測試覆蓋率篇中都會使用的一張圖。
這個一個函數的流程圖,需要傳入三個參數;有兩個語句,每個語句中有兩個判斷條件;根據傳入參數的不同,它有幾種可能的執行過程:abd
、abe
等等。牢記這張圖。
我們用Python代碼實現這個函數:
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 or x > 1: x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=2, b=1, x=3)
上面的示例中,我們分別寫了兩個用例case1
和case2
。
那么它們的用例怎么設計呢?又怎么計算語句覆蓋率呢?
在測試時,首先設計若干個測試用例,然后運行被測程序,使程序中的每個可執行語句至少執行一次。
首先來看case1
,在函數foo
中,因為每個語句都包含兩個子條件,所有共有4個語句,當然,你也可以只根據大的語句來划分,那就是2個語句。
根據case1
的傳入參數a=2, b=0, x=3
,它符合:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1 | T |
2 | B==0 | T |
3 | A==2 | T |
4 | X>3 | F |
在程序運行中,上面4種情況都會覆蓋到,所以,根據上面的公式:
被執行的語句是4條除以可執行的語句總數4條,它的覆蓋率是100%。
而case2
,根據傳入參數a=2, b=1, x=3
:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1, B==0 | T,F |
2 | A==2, X>1 | T,F |
因為and的關系,上面第一條語句不會執行。所以,它的覆蓋率50%。
需要注意的是,有時候語句覆蓋率達到100%也會有缺陷發現不了,比如開發人員將第二個語句寫成了and
:
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 and x > 1: # 偷偷將 or 替換為 and x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=2, b=1, x=3)
自己算覆蓋率吧!
所以覆蓋率只是我們度量的一個手段。
判定覆蓋率
判定覆蓋(Decision Coverage)也叫分支覆蓋(Branch Coverage),它是指在測試時運行被測試程序后,程序中所有判定語句的取真分支和取假被執行到的比率:
判定覆蓋率 = (判定結果被評價的次數) / (判定結果的總數)
一般的判定條件可能有這些:if
、if\else
、if\elif\else
等等。
這個怎么玩呢?把最開始的程序圖拿過來(你想象它在這!)。
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 or x > 1: x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=1, b=0, x=1)
我們設計了兩個用例,來判斷取真取假的情況。
來看case1
,參數是a=2, b=0, x=3
:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1, B==0 | T |
2 | A==2, X>1 | T |
在來看case2
,參數是a=1, b=0, x=1
:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1, B==0 | F |
2 | A==2, X>1 | F |
case1
走了兩次真,case2
走了兩次假,我們認為這兩個用例的判定覆蓋率達到了100%:
需要注意的是,有時候語句覆蓋率達到100%也會有缺陷發現不了,比如開發人員將第二個條件語句的X>1
寫成了X<1
它的執行情況是:
用例 | 編號 | 條件取值 | 標記 |
---|---|---|---|
case1 | 1 | A>1, B==0 | T |
case1 | 2 | A==2, X>1 | T |
case2 | 3 | A>1, B==0 | F |
case2 | 4 | A==2, X<1 | F |
此時的判定覆蓋率仍然為100%,但是程序已經存在問題了。
條件覆蓋率
條件覆蓋(Condition Coverage)的含義時,在測試時運行被測程序后,所有的判斷語句中每個條件的可能取值(真和假)出現過的比率:
條件覆蓋率 = (條件操作數值至少被評價一次的數量) / (條件操作數的總數)
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 or x > 1: x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=1, b=0, x=1) case3 = foo(a=2, b=1, x=1)
這個又該怎么玩呢?
我們在設計用例時,要使每個判斷中的每個條件的可能值都要滿足一次,這么說,上面的程序有4個語句,每個語句都有兩種情況,所以,共有八種情況:
條件 | 取真 | 取假 | 標記 |
---|---|---|---|
A>1 | T | F | T,F |
B==0 | T | F | T,F |
A==2 | T | F | T,F |
X>1 | T | F | T,F |
設計測試用例(看着那個圖!):
測試用例 | 參數:A B X | 執行路徑 | 覆蓋條件 |
---|---|---|---|
case1 | 203 | ace | T,T,T,T |
case2 | 101 | abd | F,T,F,F |
case3 | 211 | abe | T,F,T,F |
case1
執行了四個真,case2
執行了三個假,漏掉一個假,所以,我們又設計了case3
來補上這個假,最終它們的條件覆蓋率是:
八種情況都覆蓋了。
沒完!覆蓋了條件的測試用例不一定覆蓋了分支:
測試用例 | 參數:A B X | 執行路徑 | 覆蓋條件 |
---|---|---|---|
case1 | 103 | abe | F,T,F,T |
case2 | 211 | abe | T,FT,F |
八種條件都走完了,但是用例少覆蓋了分支acd
。
判定條件覆蓋率
判定條件覆蓋率(Decision Condition Coverage)也叫分支條件覆蓋率(Branch Condition Coverage)。所謂判定條件覆蓋就是設計足夠的測試用例,使得判斷中每個條件的所有可能取值至少執行一次,同時每個判斷本身的所有可能判斷結果至少執行一次:
判定條件覆蓋率 = (條件操作數值或判定結果至少被評價一次的數量) / (條件操作數值 + 判定結果總數)
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 or x > 1: x = x + 1 case1 = foo(a=2, b=1, x=3) case2 = foo(a=0, b=0, x=1)
我們先單獨算出來case1
的條件覆蓋率和判定覆蓋率。
先來看條件的:
條件 | 取真 | 取假 | 標記 |
---|---|---|---|
A>1 | T | F | T,F |
B==0 | T | F | T,F |
A==2 | T | F | T,F |
X>1 | T | F | T,F |
因為是8個條件。所以結果是4/8
,覆蓋率也是50%。
再來看判定,參數是a=2, b=1, x=3
:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1, B==0 | T,F |
2 | A==2,X>1 | T,T |
雖然4個條件得的結果是TFTT
,但由於第一個條件是and,而結果是TF
,所以,不執行,所以結果是2/4
,覆蓋率是50%。
那么判定條件覆蓋率怎么算呢?
在來個示例,看看case2
的,參數是a=0, b=0, x=1
。
先來看條件的:
編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1, B==0 | F,T |
2 | A==2,X>1 | F,F |
因為是8個條件(四個語句分別取真假)。所以結果是4/8
,覆蓋率也是50%。
再來看判定,參數是a=0, b=0, x=1
:
條件 | 取真 | 取假 | 標記 |
---|---|---|---|
A>1,B==0 | T | F | F,T |
A==2,X>1 | T | F | F,F |
4個語句得的結果是FTFF
,所以結果是2/4
,覆蓋率是50%。
最后的判定條件覆蓋率仍然是6/12
。
條件組合覆蓋率
再說條件覆蓋率之前先來看個示例:
def foo(a, b, x): if a > 1 and b == 0: x /= a if a == 2 or x > 1: x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=1, b=1, x=1)
上例中,兩個用例的判定:
用例名 | 取值 | 標記 |
---|---|---|
case1:203 | A>1,B==0 | T,T |
case1:203 | A==2,X>1 | T,T |
case2:111 | A>1,B==0 | F,T |
case2:111 | A==2,X>1 | F,F |
匯總一下,兩個用例的判定都覆蓋到了,所以是4/4
。
而條件覆蓋率:
用例名 | 取值 | 標記 |
---|---|---|
case1:203 | A>1,B==0 | T,T |
case1:203 | A==2,X>1 | T,T |
case2:111 | A>1,B==0 | F,F |
case2:111 | A==2,X>1 | F,F |
8個條件都覆蓋到了,所條件覆蓋率是8/8
,那么,它們的判定條件覆蓋率是12/12
:
也就是說,現在判定條件覆蓋率是100%。
現在,開發把上面兩個條件的and
和or
條件互換了。那么用上面的兩個case它們的判定條件覆蓋率同樣是100%。
def foo(a, b, x): if a > 1 or b == 0: x /= a if a == 2 and x > 1: x = x + 1 case1 = foo(a=2, b=0, x=3) case2 = foo(a=1, b=1, x=1)
判定:
用例 | 取值 | 標記 |
---|---|---|
case1:203 | A>1,B==0 | T,T |
case1:203 | A==2,X>1 | T,T |
case1:111 | A>1,B==0 | F,F |
case2:111 | A==2,X>1 | F,F |
兩個用例的判定仍然是4/4
。條件是8/8
,那么判定條件覆蓋率是12/12
,最終的結果是100%。所以開發寫錯了代碼,這兩個用例仍然沒有發現這類問題。
針對這類問題,就要使用條件組合覆蓋率了。
條件組合覆蓋(Multiple Condition Coverage)的基本思想是設計足夠的測試用例,使每個判定中條件的各種可能的組合都至少出現一次:
條件組合覆蓋率 = (條件組合至少被評價一次的數量) / (條件組合總數)
同樣是上面的示例。
每個判斷中所有的組合都要覆蓋到。
組合編號 | 條件取值 | 標記 |
---|---|---|
1 | A>1,B==0 | T,T |
2 | A>1,B!=0 | T,F |
3 | A<=1,B==0 | F,T |
4 | A<=1,B!=0 | F,F |
5 | A==2,X>1 | T,F |
6 | A==2,X<=1 | T,F |
7 | A!=2,X>1 | F,T |
8 | A!=2,X<=1 | F,F |
兩個判定,每個判定中有兩個條件,所有共有八種條件,所以,要想達到條件組合覆蓋率100%的話,這八種情況都要覆蓋到。
測試用例 | 參數:A B X | 覆蓋組合編號 | 所走路徑 | 覆蓋條件 |
---|---|---|---|---|
case1 | 203 | 1,5 | ace | TTTT |
case2 | 211 | 2,6 | abe | TFTF |
case3 | 103 | 3,7 | abe | FTFT |
case4 | 111 | 4,8 | abd | FFFF |
PS:覆蓋組合編號內容是上上表中的組合編號。
上例中的四個測試用例覆蓋了所有的條件、分支。
所以,條件組合覆蓋率是100%的話,那么,條件、判定、語句都會達到100%。
但從程序運行路徑角度來看,僅覆蓋了3條路徑,遺漏掉了acd。
所以,目前為止,並沒有一種覆蓋率能保證測試覆蓋率達到100%。
路徑覆蓋率
路徑覆蓋(Path Coverage)是指在測試時運行被測程序后, 程序中所有可能的路徑被執行過的比率。
路徑覆蓋率 = (至少被執行到一次的路徑數) / (總的路徑數)
還是那個示例,還是那個圖!
如果你看着圖的話, 程序執行的路徑有以下幾條:
測試用例 | 參數:A B X | 覆蓋路徑 |
---|---|---|
case1 | 203 | ace |
case2 | 101 | abd |
case3 | 211 | abe |
case4 | 301 | acd |
所以,我們設計的測試用以,要把這些路徑都要覆蓋到。
看着采用路徑覆蓋率好使,但是仍然不是完美的。
如果開發將程序寫錯成了:
def foo(a, b, x): if a > 1 and b == 0: x /= a # if a == 2 or x > 1: # 正確的 if a == 2 and x >= 1: # 開發寫錯后的 x = x + 1
那么,在這個錯誤示例中,用上面的幾個用例,仍然每個路徑都能覆蓋到,但是這個錯誤卻不能發掘出來。
所以說,每種覆蓋率都有自己的局限性,因此在測試中, 要把各種覆蓋率組合起來對測試用例進行綜合考量。
一般的,我們選擇覆蓋率優先級是:
- 語句;
- 判定;
- 條件;
- 條件組合;
- 路徑;
在白盒這里的幾種覆蓋率,大致就是這些,我們來看其他的。
灰盒覆蓋率
灰盒覆蓋率這主要包括:
- 函數覆蓋;
- 接口覆蓋;
函數覆蓋
函數覆蓋(Function Coverage)是針對系統或者一個子系統測試,它表示在該測試中,有那些函數被測試到了。其被測到的頻率有多大,這些函數在系統所有函數中的比例有多大,函數覆蓋是一個比較容易自動化的技術:
函數覆蓋 = (至少被執行一次的函數數量) / (系統中函數的總數)
假如有100個函數,在測試過程中,有90個函數(至少被執行了一次的函數)被測試到了, 所以它的覆蓋率是90%。
也有很多測試工具提供了了函數覆蓋,比如說TrueCoverage
、PureCoverage
等。
接口覆蓋率
接口覆蓋(Interface Coverage)也叫做入口點覆蓋(Entry-Point Coverage),要求通過設計一定的用例使系統的每個接口被測試到:
接口覆蓋率 = (至少被執行一次的接口數量) / (系統中接口的總數)
被測試的接口可能是對外提供訪問的接口,也可能是內部接口。
接口可是是函數與函數之間,模塊與模塊之間,子系統與子系統之間的接口。
我們主要關注接口之間的數據交換、有沒有邏輯依賴等等。
比如有兩條測試用例去測試4個接口,那么覆蓋到了其中的兩個,那么它的接口覆蓋率是2/4
。
黑盒覆蓋率
黑盒測試主要應用在系統測試階段,那么在系統測試階段,它的測試過程一般是這樣的。
- 根據需求提取需求項;
- 根據需求項提取功能點;
- 根據功能點設計測試用例。
所以,在黑盒覆蓋率這塊,主要就是做功能覆蓋率(Functional Coverage)。
而在功能覆蓋率中最常見的是需求覆蓋(Requirement Coverage),其含義是通過設計一定的測試用例,要求每個需求點都被測試到。
需求覆蓋率 = (被驗證到的需求數量) / (總的需求數量)
功能覆蓋方面的自動化工具比較少。
當然,以上是針對系統測試階段來划分的。
當然,單元測試階段,也能進行黑盒測試。
比如,我們對一個函數進行測試。我們可以把函數當成一個黑匣子,我們關注入參和返回的兩邊數據即可,而不用特意關注函數內部的實現。
面向對象覆蓋率
結構化覆蓋率用來度量測試的完整性已經被大家所接受,但是這個技術在面向對象領域卻遇到挑戰,由於傳統的結構化度量沒有考慮面向對象的一些特性,如繼承、封裝、多態。
繼承對覆蓋率度量的影響
class Base(object): def foo(self): pass def bar(self): pass class Derived(Base): def foo(self): pass case1 = Derived().foo() case2 = Derived().bar() # 要考慮到面向對象的繼承關系 case3 = Base().foo() case4 = Base().bar()
上例中,如果我們的用例不僅要測試到派生類的兩個方法,雖然它背后也調用了基類的方法,但要考慮到繼承的概念,要讓基類自己執行自己的方法。
基於狀態的類的覆蓋率特點
來舉個棧的示例。
class Stack(object): def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): self.items.pop() p = Stack() p.push('a') # case1 p.pop() # case2
現在,站在灰盒測試的角度來說,通過測試出棧和入棧的操作,已經達到了測試的效果了。
這就完了么?沒有!因為我們沒有考慮到棧的特點,比如,我們有沒有考慮,如果棧為空,你做出棧操作,會不會拋出異常?
如果這是個有邊界限制的棧,棧滿了,你還能做入棧操作嗎?
所以,我們要考慮到棧的各種情況。
根據這些狀態,來針對性寫用例。
最后,從測試方法來說,有白盒、黑盒、灰盒。
當然,在平日沒有必要將彼此划分的很清楚,因為我們學習覆蓋率的目的是用來度量測試完整性的手段,只是驗證測試效果的一種衡量標准。
還是要根據項目、測試階段、測試方法不同,來采用不同的測試覆蓋率。