第十一章:測試和質(zhì)量保障

2018-02-24 15:49 更新

第十一章:測試和質(zhì)量保障

構(gòu)建真實系統(tǒng)意味著我們要關(guān)心系統(tǒng)的質(zhì)量控制,健壯性和正確性。有了正確的質(zhì)量保障機(jī)制,良好編寫的代碼才能像一架精確的機(jī)器一樣,所有模塊都完成它們預(yù)期的任務(wù),并且不會有模棱兩可的邊界情況。最后我們得到的將是不言自明,正確無疑的代碼——這樣的代碼往往能激發(fā)自信。

Haskell 有幾個工具用來構(gòu)建這樣精確的系統(tǒng)。最明顯的一個,也是語言本身就內(nèi)置的,是具有強(qiáng)大表達(dá)力的類型系統(tǒng)。它使得一些復(fù)雜的不變量(invariants)得到了靜態(tài)保證——絕無可能寫出違反這些約束條件的代碼。另外,純度和多態(tài)也促進(jìn)了模塊化,易重構(gòu),易測試的代碼風(fēng)格。這種類型的代碼通常不會出錯。

測試在保證代碼的正確性上起到了關(guān)鍵作用。Haskell 主要的測試機(jī)制是傳統(tǒng)的單元測試(通過 HUnit 庫)和由它衍生而來的更強(qiáng)機(jī)制:使用 Haskell 開源測試框架 QuickCheck 進(jìn)行的基于類型的“性質(zhì)”測試?;谛再|(zhì)的測試是一種層次較高的方法,它抽象出一些函數(shù)應(yīng)該普遍滿足的不變量,真正的測試數(shù)據(jù)由測試庫為程序員產(chǎn)生。通過這種方法,我們可以用成百上千的測試來檢驗代碼,從而發(fā)現(xiàn)一些用其他方法無法發(fā)現(xiàn)的微妙的邊角情形(corner cases),而這對于手寫來說是不可能的。

在這章里,我們將會學(xué)習(xí)如何使用 QuickCheck 來建立不變量,然后重新審視之前章節(jié)開發(fā)的美觀打印器,并用 QuickCheck 對它進(jìn)行測試。我們也會學(xué)習(xí)如何用 GHC 的內(nèi)置代碼覆蓋工具 HPC 來指導(dǎo)測試過程。

QuickCheck: 基于類型的測試

為了大概了解基于性質(zhì)的測試是如何工作的,我們從一個簡單的情形開始:你寫了一個排序算法,需要測試它的行為。

首先我們載入 QuickCheck 庫和其它依賴模塊:

-- file: ch11/QC-basics.hs
import Test.QuickCheck
import Data.List

然后是我們想要測試的函數(shù)——一個自定義的排序過程:

-- file: ch11/QC-basics.hs
qsort :: Ord a => [a] -> [a]
qsort []     = []
qsort (x:xs) = qsort lhs ++ [x] ++ qsort rhs
    where lhs = filter  (< x) xs
          rhs = filter (>= x) xs

這是一個經(jīng)典的 Haskell 排序?qū)崿F(xiàn):它可能不夠高效(因為不是原地排序),但它至少展示了函數(shù)式編程的優(yōu)雅?,F(xiàn)在,我們來檢查這個函數(shù)是否符合一個好排序算法應(yīng)該符合的基本規(guī)則。很多純函數(shù)式代碼都有的一個很有用的不變量是冪等(idempotency)——應(yīng)用一個函數(shù)兩次和一次效果應(yīng)該相同。對于我們的排序過程,一個穩(wěn)定的排序算法,這當(dāng)然應(yīng)該滿足,否則就真的出大錯了!這個不變量可以簡單地表示為如下性質(zhì):

-- file: ch11/QC-basics.hs
prop_idempotent xs = qsort (qsort xs) == qsort xs

依照 QuickCheck 的慣例,我們給測試性質(zhì)加上 prop_ 前綴以和普通代碼區(qū)分。冪等性質(zhì)可以簡單地用一個 Haskell 函數(shù)表示:對于任何已排序輸入,再次應(yīng)用 qsort 結(jié)果必須相同。我們可以手動寫幾個例子來確保沒什么問題:

[譯注,運(yùn)行之前需要確保自己安裝了 QuickCheck 包,譯者使用的版本是2.8.1。]

ghci> prop_idempotent []
True
ghci> prop_idempotent [1,1,1,1]
True
ghci> prop_idempotent [1..100]
True
ghci> prop_idempotent [1,5,2,1,2,0,9]
True

看起來不錯。但是,用手寫輸入數(shù)據(jù)非常無趣,并且違反了一個高效函數(shù)式程序員的道德法則:讓機(jī)器干活!為了使這個過程自動化,QuickCheck 內(nèi)置了一組數(shù)據(jù)生成器用來生成 Haskell 所有的基本數(shù)據(jù)類型。QuickCheck 使用 Arbitrary 類型類來給(偽)隨機(jī)數(shù)據(jù)生成過程提供了一個統(tǒng)一接口,類型系統(tǒng)會具體決定使用哪個生成器。QuickCheck 通常會把數(shù)據(jù)生成過程隱藏起來,但我們可以手動運(yùn)行生成器來看看 QuickCheck 生成的數(shù)據(jù)呈什么分布。例如,隨機(jī)生成一組布爾值:

[譯注:本例子根據(jù)最新版本的 QuickCheck 庫做了改動。]

Prelude Test.QuickCheck.Gen Test.QuickCheck.Arbitrary> sample' arbitrary :: IO [Bool]
[False,False,False,True,False,False,True,True,True,True,True]

QuickCheck 用這種方法產(chǎn)生測試數(shù)據(jù),然后通過 quickCheck 函數(shù)把數(shù)據(jù)傳給我們要測試的性質(zhì)。性質(zhì)本身的類型決定了它使用哪個數(shù)據(jù)生成器。quickCheck 確保對于所有產(chǎn)生的測試數(shù)據(jù),性質(zhì)仍然成立。由于冪等測試對于列表元素類型是多態(tài)的,我們需要選擇一個特定的類型來產(chǎn)生測試數(shù)據(jù),我們把它作為一個類型約束寫在性質(zhì)上。運(yùn)行測試的時候,只需調(diào)用 quickCheck 函數(shù),并指定我們性質(zhì)函數(shù)的類型即可(否則的話,列表值將會是沒什么意思的 () 類型):

*Main Test.QuickCheck> :type quickCheck
quickCheck :: Testable prop => prop -> IO ()
*Main Test.QuickCheck> quickCheck (prop_idempotent :: [Integer] -> Bool)
+++ OK, passed 100 tests.

對于產(chǎn)生的100個不同列表,我們的性質(zhì)都成立——太棒了!編寫測試的時候,查看為每個測試生成的實際數(shù)據(jù)常常會很有用。我們可以把 quickCheck 替換為它的兄弟函數(shù) verboseCheck 來查看每個測試的(完整)輸出。現(xiàn)在,來看看我們的函數(shù)還可能滿足什么更復(fù)雜的性質(zhì)。

性質(zhì)測試

好的庫通常都會包含一組彼此正交而又關(guān)聯(lián)的基本函數(shù)。我們可以使用 QuickCheck 來指定我們代碼中函數(shù)之間的關(guān)系,從而通過一組通過有用性質(zhì)相互關(guān)聯(lián)的函數(shù)來提供一個好的庫接口。從這個角度來說,QuickCheck 扮演了 API “l(fā)int” 工具的角色:它確保我們的庫 API 能說的通。

列表排序函數(shù)的一些有趣性質(zhì)把它和其它列表操作關(guān)聯(lián)起來。例如:已排序列表的第一個元素應(yīng)該是輸入列表的最小元素。我們可以使用 List 庫的 minimum 函數(shù)來指出這個性質(zhì):

-- file: ch11/QC-basics.hs
import Data.List
prop_minimum xs         = head (qsort xs) == minimum xs

測試的時候出錯了:

*Main Test.QuickCheck> quickCheck (prop_minimum :: [Integer] -> Bool)
*** Failed! Exception: 'Prelude.head: empty list' (after 1 test):
[]

當(dāng)對一個空列表排序時性質(zhì)不滿足了:對于空列表而言,head 和 minimum 沒有定義,正如它們的定義所示:

-- file: ch11/minimum.hs
head       :: [a] -> a
head (x:_) = x
head []    = error "Prelude.head: empty list"

minimum    :: (Ord a) => [a] -> a
minimum [] =  error "Prelude.minimum: empty list"
minimum xs =  foldl1 min xs

因此這個性質(zhì)只在非空列表上滿足。幸運(yùn)的是,QuickCheck 內(nèi)置了一套完整的性質(zhì)編寫語言,使我們可以更精確地表述我們的不變量,排除那些我們不予考慮的值。對于空列表這個例子,我們可以這么說:如果列表非空,那么被排序列表的第一個元素是最小值。這是通過 (==>) 函數(shù)來實現(xiàn)的,它在測試性質(zhì)之前將無效數(shù)據(jù)排除在外:

-- file: ch11/QC-basics.hs
prop_minimum' xs         = not (null xs) ==> head (qsort xs) == minimum xs

結(jié)果非常清楚。通過把空列表排除在外,我們可以確定指定性質(zhì)是成立的。

*Main Test.QuickCheck> quickCheck (prop_minimum' :: [Integer] -> Property)
+++ OK, passed 100 tests.

注意到我們把性質(zhì)的類型從 Bool 改成了更一般的 Property 類型(property 函數(shù)會在測試之前過濾出非空列表,而不僅是簡單地返回一個布爾常量了)。

再加上其它一些應(yīng)該滿足的不變量,我們就可以完成排序函數(shù)的基本性質(zhì)集了:輸出應(yīng)該有序(每個元素應(yīng)該小于等于它的后繼元素);輸出是輸入的排列(我們通過列表差異函數(shù) (\) 來檢測);被排序列表的最后一個元素應(yīng)該是最大值;對于兩個不同列表的最小值,如果我們把兩個列表拼接并排序,這個值應(yīng)該是第一個元素。這些性質(zhì)可以表述如下:

-- file: ch11/QC-basics.hs
prop_ordered xs = ordered (qsort xs)
    where ordered []       = True
          ordered [x]      = True
          ordered (x:y:xs) = x <= y && ordered (y:xs)

prop_permutation xs = permutation xs (qsort xs)
    where permutation xs ys = null (xs \\ ys) && null (ys \\ xs)

prop_maximum xs         =
    not (null xs) ==>
        last (qsort xs) == maximum xs

prop_append xs ys       =
    not (null xs) ==>
    not (null ys) ==>
        head (qsort (xs ++ ys)) == min (minimum xs) (minimum ys)

利用模型進(jìn)行測試

另一種增加代碼可信度的技術(shù)是利用模型實現(xiàn)進(jìn)行測試。我們可以把我們的列表排序函數(shù)跟標(biāo)準(zhǔn)列表庫中的排序?qū)崿F(xiàn)進(jìn)行對比。如果它們行為相同,我們會有更多信心我們的代碼的正確的。

-- file: ch11/QC-basics.hs
prop_sort_model xs      = sort xs == qsort xs

這種基于模型的測試非常強(qiáng)大。開發(fā)人員經(jīng)常會有一些正確但低效的參考實現(xiàn)或原型。他們可以保留這部分代碼來確保優(yōu)化之后的生產(chǎn)代碼仍具有相同行為。通過構(gòu)建大量這樣的測試并定期運(yùn)行(例如每次提交),我們可以很容易地確保代碼仍然正確。大型的 Haskell 項目通常包含了跟項目本身大小可比的性質(zhì)測試集,每次代碼改變都會進(jìn)行成千上萬項不變量測試,保證了代碼行為跟預(yù)期一致。

測試案例學(xué)習(xí):美觀打印器

測試單個函數(shù)的自然性質(zhì)是開發(fā)大型 Haskell 系統(tǒng)的基石。我們現(xiàn)在來看一個更復(fù)雜的案例:為第五章開發(fā)的美觀打印器編寫測試集。

生成測試數(shù)據(jù)

美觀打印器是圍繞 Doc 而建的,它是一個代數(shù)數(shù)據(jù)類型,表示格式良好的文檔。

-- file: ch11/Prettify2.hs

data Doc = Empty
         | Char Char
         | Text String
         | Line
         | Concat Doc Doc
         | Union Doc Doc
         deriving (Show,Eq)

這個庫本身是由一組函數(shù)構(gòu)成的,這些函數(shù)負(fù)責(zé)構(gòu)建和變換 Doc 類型的值,最后再把它們轉(zhuǎn)換成字符串。

QuickCheck 鼓勵這樣一種測試方式:開發(fā)人員指定一些不變量,它們對于任何代碼接受的輸入都成立。為了測試美觀打印庫,我們首先需要一個輸入數(shù)據(jù)源。我們可以利用 QuickCheck 通過 Arbitrary 類型類提供的一套用來生成隨機(jī)數(shù)據(jù)的組合子集。Arbitrary 類型類提供了 arbitrary 函數(shù)來給每種類型生成數(shù)據(jù),我們可以利用它來給自定義數(shù)據(jù)類型寫數(shù)據(jù)生成器。

-- file: ch11/Arbitrary.hs
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Gen
class Arbitrary a where
    arbitrary   :: Gen a

有一點需要注意,函數(shù)的類型簽名表明生成器運(yùn)行在 Gen 環(huán)境中。它是一個簡單的狀態(tài)傳遞 monad,用來隱藏貫穿于代碼中的隨機(jī)數(shù)字生成器的狀態(tài)。稍后的章節(jié)會更加細(xì)致地研究 monads,現(xiàn)在只要知道,由于 Gen 被定義為一個 monad,我們可以使用 do 語法來定義新生成器來訪問隱式的隨機(jī)數(shù)字源。Arbitrary 類型類提供了一組可以生成隨機(jī)值的函數(shù),我們可以把它們組合起來構(gòu)建出我們所關(guān)心的類型的數(shù)據(jù)結(jié)構(gòu),以便給我們的自定義類型寫生成器。一些關(guān)鍵函數(shù)的類型如下:

-- file: ch11/Arbitrary.hs
    elements :: [a] -> Gen a
    choose   :: Random a => (a, a) -> Gen a
    oneof    :: [Gen a] -> Gen a

elements 函數(shù)接受一個列表,返回這個列表的隨機(jī)值生成器。我們稍后再用 choose 和 oneof。有了 elements,我們就可以開始給一些簡單的數(shù)據(jù)類型寫生成器了。例如,如果我們給三元邏輯定義了一個新數(shù)據(jù)類型:

-- file: ch11/Arbitrary.hs
data Ternary
    = Yes
    | No
    | Unknown
    deriving (Eq,Show)

我們可以給 Ternary 類型實現(xiàn) Arbitrary 實例:只要實現(xiàn) arbitrary 即可,它從所有可能的 Ternary 類型值中隨機(jī)選出一些來:

-- file: ch11/Arbitrary.hs
instance Arbitrary Ternary where
    arbitrary     = elements [Yes, No, Unknown]

另一種生成數(shù)據(jù)的方案是生成 Haskell 基本類型數(shù)據(jù),然后把它們映射成我們感興趣的類型。在寫 Ternary 實例的時候,我們可以用 choose 生成0到2的整數(shù)值,然后把它們映射為 Ternary 值。

-- file: ch11/Arbitrary2.hs
instance Arbitrary Ternary where
    arbitrary     = do
        n <- choose (0, 2) :: Gen Int
        return $ case n of
                      0 -> Yes
                      1 -> No
                      _ -> Unknown

對于簡單的類型,這種方法非常奏效,因為整數(shù)可以很好地映射到數(shù)據(jù)類型的構(gòu)造器上。對于類型(如結(jié)構(gòu)體和元組),我們首先得把積的不同部分分別生成(對于嵌套類型遞歸地生成),然后再把他們組合起來。例如,生成隨機(jī)序?qū)Γ?/p>

-- file: ch11/Arbitrary.hs
instance (Arbitrary a, Arbitrary b) => Arbitrary (a, b) where
    arbitrary = do
        x <- arbitrary
        y <- arbitrary
        return (x, y)

現(xiàn)在我們寫個生成器來生成 Doc 類型所有不同的變種。我們把問題分解,首先先隨機(jī)生成一個構(gòu)造器,然后根據(jù)結(jié)果再隨機(jī)生成參數(shù)。最復(fù)雜的是 union 和 concatenation 這兩種情形。

[譯注,作者在此處解釋并實現(xiàn)了 Char 的 Arbitrary 實例。但由于最新 QuickCheck 已經(jīng)包含此實例,故此處略去相關(guān)內(nèi)容。]

現(xiàn)在我們可以開始給 Doc 寫實例了。只要枚舉構(gòu)造器,再把參數(shù)填進(jìn)去即可。我們用一個隨機(jī)整數(shù)來表示生成哪種形式的 Doc,然后再根據(jù)結(jié)果分派。生成 concat 和 union 的 Doc 值時,我們只需要遞歸調(diào)用 arbitrary 即可,類型推導(dǎo)會決定使用哪個 Arbitrary 實例:

-- file: ch11/QC.hs
instance Arbitrary Doc where
    arbitrary = do
        n <- choose (1,6) :: Gen Int
        case n of
             1 -> return Empty

             2 -> do x <- arbitrary
                     return (Char x)

             3 -> do x <- arbitrary
                     return (Text x)

             4 -> return Line

             5 -> do x <- arbitrary
                     y <- arbitrary
                     return (Concat x y)

             6 -> do x <- arbitrary
                     y <- arbitrary
                     return (Union x y)

看起來很直觀。我們可以用 oneof 函數(shù)來化簡它。我們之前見到過 oneof 的類型,它從列表中選擇一個生成器(我們也可以用 monadic 組合子 liftM 來避免命名中間結(jié)果):

-- file: ch11/QC.hs
instance Arbitrary Doc where
    arbitrary =
        oneof [ return Empty
              , liftM  Char   arbitrary
              , liftM  Text   arbitrary
              , return Line
              , liftM2 Concat arbitrary arbitrary
              , liftM2 Union  arbitrary arbitrary ]

后者更簡潔。我們可以試著生成一些隨機(jī)文檔,確保沒什么問題。

*QC Test.QuickCheck> sample' (arbitrary::Gen Doc)
[Text "",Concat (Char '\157') Line,Char '\NAK',Concat (Text "A\b") Empty,
Union Empty (Text "4\146~\210"),Line,Union Line Line,
Concat Empty (Text "|m  \DC4-\DLE*3\DC3\186"),Char '-',
Union (Union Line (Text "T\141\167\&3\233\163\&5\STX\164\145zI")) (Char '~'),Line]

從輸出的結(jié)果里,我們既看到了簡單,基本的文檔,也看到了相對復(fù)雜的嵌套文檔。每次測試時我們都會隨機(jī)生成成百上千的隨機(jī)文檔,他們應(yīng)該可以很好地覆蓋各種情形?,F(xiàn)在我們可以開始給我們的文檔函數(shù)寫一些通用性質(zhì)了。

測試文檔構(gòu)建

文檔有兩個基本函數(shù):一個是空文檔常量 Empty,另一個是拼接函數(shù)。它們的類型是:

-- file: ch11/Prettify2.hs
empty :: Doc
(<>)  :: Doc -> Doc -> Doc

兩個函數(shù)合起來有一個不錯的性質(zhì):將空列表拼接在(無論是左拼接還是右拼接)另一個列表上,這個列表保持不變。我們可以將這個不變量表述為如下性質(zhì):

-- file: ch11/QC.hs
prop_empty_id x =
    empty <> x == x
  &&
    x <> empty == x

運(yùn)行測試,確保性質(zhì)成立:

*QC Test.QuickCheck> quickCheck prop_empty_id
+++ OK, passed 100 tests.

可以把 quickCheck 替換成 verboseCheck 來看看實際測試時用的是哪些文檔。從輸出可以看到,簡單和復(fù)雜的情形都覆蓋到了。如果需要的話,我們還可以進(jìn)一步優(yōu)化數(shù)據(jù)生成器來控制不同類型數(shù)據(jù)的比例。

其它 API 函數(shù)也很簡單,可以用性質(zhì)來完全描述它們的行為。這樣做使得我們可以對函數(shù)的行為維護(hù)一個外部的,可檢查的描述以確保之后的修改不會破壞這些基本不變量:

-- file: ch11/QC.hs

prop_char c   = char c   == Char c

prop_text s   = text s   == if null s then Empty else Text s

prop_line     = line     == Line

prop_double d = double d == text (show d)

這些性質(zhì)足以測試基本的文檔結(jié)構(gòu)了。測試庫的剩余部分還要更多工作。

以列表為模型

高階函數(shù)是可復(fù)用編程的基本膠水,我們的美觀打印庫也不例外——我們自定義了 fold 函數(shù),用來在內(nèi)部實現(xiàn)文檔拼接和在文檔塊之間加分隔符。fold 函數(shù)接受一個文檔列表,并借助一個合并方程(combining function)把它們粘合在一起。

-- file: ch11/Prettify2.hs
fold :: (Doc -> Doc -> Doc) -> [Doc] -> Doc
fold f = foldr f empty

我們可以很容易地給某個特定 fold 實例寫測試。例如,橫向拼接(Horizontal concatenation)就可以簡單地利用列表中的參考實現(xiàn)來測試。

-- file: ch11/QC.hs

prop_hcat xs = hcat xs == glue xs
    where
        glue []     = empty
        glue (d:ds) = d <> glue ds

punctuate 也類似,插入標(biāo)點類似于列表的 interspersion 操作(intersperse 這個函數(shù)來自于 Data.List,它把一個元素插在列表元素之間):

-- file: ch11/QC.hs

prop_punctuate s xs = punctuate s xs == intersperse s xs

看起來不錯,運(yùn)行起來卻出了問題:

*QC Test.QuickCheck> quickCheck prop_punctuate
*** Failed! Falsifiable (after 5 tests and 1 shrink):
Empty
[Text "",Text "E"]

美觀打印庫優(yōu)化了冗余的空文檔,然而模型實現(xiàn)卻沒有,所以我們得讓模型匹配實際情況。首先,我們可以把分隔符插入文檔,然后再用一個循環(huán)去掉當(dāng)中的 Empty 文檔,就像這樣:

-- file: ch11/QC.hs
prop_punctuate' s xs = punctuate s xs == combine (intersperse s xs)
    where
        combine []           = []
        combine [x]          = [x]

        combine (x:Empty:ys) = x : combine ys
        combine (Empty:y:ys) = y : combine ys
        combine (x:y:ys)     = x `Concat` y : combine ys

ghci 里運(yùn)行,確保結(jié)果是正確的。測試框架發(fā)現(xiàn)代碼中的錯誤讓人感到欣慰——因為這正是我們追求的。

*QC Test.QuickCheck> quickCheck prop_punctuate'
+++ OK, passed 100 tests.

完成測試框架

[譯注:為了匹配最新版本的 QuickCheck,本節(jié)在原文基礎(chǔ)上做了較大改動。讀者可自行參考原文,對比閱讀。]

我們可以把這些測試單獨放在一個文件中,然后用 QuickCheck 的驅(qū)動函數(shù)運(yùn)行它們。這樣的函數(shù)有很多,包括一些復(fù)雜的并行驅(qū)動函數(shù)。我們在這里使用 quickCheckWithResult 函數(shù)。我們只需提供一些測試參數(shù),然后列出我們想要測試的函數(shù)即可:

-- file: ch11/Run.hs
module Main where
import QC
import Test.QuickCheck

anal :: Args
anal = Args
    { replay = Nothing
    , maxSuccess = 1000
    , maxDiscardRatio = 1
    , maxSize = 1000
    , chatty = True
    }

minimal :: Args
minimal = Args
    { replay = Nothing
    , maxSuccess = 200
    , maxDiscardRatio = 1
    , maxSize = 200
    , chatty = True
    }

runTests :: Args -> IO ()
runTests args = do
    f prop_empty_id "empty_id ok?"
    f prop_char "char ok?"
    f prop_text "text ok?"
    f prop_line "line ok?"
    f prop_double "double ok?"
    f prop_hcat "hcat ok?"
    f prop_punctuate' "punctuate ok?"
    where
        f prop str = do
            putStrLn str
            quickCheckWithResult args prop
            return ()

main :: IO ()
main = do
    putStrLn "Choose test depth"
    putStrLn "1. Anal"
    putStrLn "2. Minimal"
    depth <- readLn
    if depth == 1
        then runTests anal
    else runTests minimal

[譯注:此代碼出處為原文下Charlie Harvey的評論。]

我們把這些代碼放在一個單獨的腳本中,聲明的實例和性質(zhì)也有自己單獨的文件,它們庫的源文件完全分開。這在庫項目中非常常見,通常在這些項目中測試都會和庫本身分開,測試通過模塊系統(tǒng)載入庫。

這時候可以編譯并運(yùn)行測試腳本了:

$ ghc --make Run.hs
[1 of 3] Compiling Prettify2        ( Prettify2.hs, Prettify2.o )
[2 of 3] Compiling QC               ( QC.hs, QC.o )
[3 of 3] Compiling Main             ( Run.hs, Run.o )
Linking Run ...
$ ./Run
Choose test depth
1. Anal
2. Minimal
2
empty_id ok?
+++ OK, passed 200 tests.
char ok?
+++ OK, passed 200 tests.
text ok?
+++ OK, passed 200 tests.
line ok?
+++ OK, passed 1 tests.
double ok?
+++ OK, passed 200 tests.
hcat ok?
+++ OK, passed 200 tests.
punctuate ok?
+++ OK, passed 200 tests.

一共產(chǎn)生了1201個測試,很不錯。增加測試深度很容易,但為了了解代碼究竟被測試的怎樣,我們應(yīng)該使用內(nèi)置的代碼覆蓋率工具 HPC,它可以精確地告訴我們發(fā)生了什么。

用 HPC 衡量測試覆蓋率

HPC(Haskell Program Coverage) 是一個編譯器擴(kuò)展,用來觀察程序運(yùn)行時哪一部分的代碼被真正執(zhí)行了。這在測試時非常有用,它讓我們精確地觀察哪些函數(shù),分支以及表達(dá)式被求值了。我們可以輕易得到被測試代碼的百分比。HPC 的內(nèi)置工具可以產(chǎn)生關(guān)于程序覆蓋率的圖表,方便我們找到測試集的缺陷。

在編譯測試代碼時,我們只需在命令行加上 -fhpc 選項,即可得到測試覆蓋率數(shù)據(jù)。

$ ghc -fhpc Run.hs --make

正常運(yùn)行測試:

$ ./Run

測試運(yùn)行時,程序運(yùn)行的細(xì)節(jié)被寫入當(dāng)前目錄下的 .tix 和 .mix 文件。之后,命令行工具 hpc 用這些文件來展示各種統(tǒng)計數(shù)據(jù),解釋發(fā)生了什么。最基本的交互是通過文字。首先,我們可以在 hpc 命令中加上 report 選項來得到一個測試覆蓋率的摘要。我們會把測試程序排除在外(使用 --exclude 選項),這樣就能把注意力集中在美觀打印庫上了。在命令行中輸入以下命令:

$ hpc report Run --exclude=Main --exclude=QC
 93% expressions used (30/32)
100% boolean coverage (0/0)
    100% guards (0/0)
    100% 'if' conditions (0/0)
    100% qualifiers (0/0)
100% alternatives used (8/8)
100% local declarations used (0/0)
 66% top-level declarations used (10/15)

[譯注:報告結(jié)果可能因人而異。]

在最后一行我們看到,測試時有66%的頂層定義被求值。對于第一次嘗試來說,已經(jīng)是很不錯的結(jié)果了。隨著被測試函數(shù)的增加,這個數(shù)字還會提升。對于快速了解結(jié)果來說文字版本的結(jié)果還不錯,但為了真正了解發(fā)生了什么,最好還是看看被標(biāo)記后的結(jié)果(marked up output)。用 markup 選項可以生成:

$hpc markup Run --exclude=Main --exclude=QC

它會對每一個 Haskell 源文件產(chǎn)生一個 html 文件,再加上一些索引文件。在瀏覽器中打開 hpc_index.html,我們可以看到一些非常漂亮的代碼覆蓋率圖表:

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號