跳轉到內容

Haskell/測試

來自 Wikibooks,開放世界中的開放書籍

Quickcheck

[編輯 | 編輯原始碼]

考慮以下函式

getList = find 5 where
     find 0 = return []
     find n = do
       ch <- getChar
       if ch `elem` ['a'..'e'] then do
             tl <- find (n-1)
             return (ch : tl) else
           find n

我們如何在 Haskell 中有效地測試此函式?我們將使用重構和 QuickCheck。

保持純淨

[編輯 | 編輯原始碼]

getList 函式很難測試,因為getChar 在外部世界執行 IO 操作,因此沒有內部方法來驗證內容。我們do塊中的其他語句都與 IO 繫結在一起。

讓我們解開我們的函式,以便我們至少可以使用 QuickCheck 測試引用透明的部分。首先,我們可以利用惰性 IO 來避免所有令人不快的底層 IO 處理。

因此,第一步是將函式的 IO 部分分解成一個薄薄的“外殼”層

-- A thin monadic skin layer
getList :: IO [Char]
getList = fmap take5 getContents

-- The actual worker
take5 :: [Char] -> [Char]
take5 = take 5 . filter (`elem` ['a'..'e'])

使用 QuickCheck 進行測試

[編輯 | 編輯原始碼]

現在,我們可以獨立測試演算法的“核心”,即 take5 函式。讓我們使用 QuickCheck。首先,我們需要 Char 型別的 Arbitrary 例項——它負責為我們生成隨機的 Char 用於測試。出於簡單起見,將其限制在一定範圍內的良好字元

import Data.Char
import Test.QuickCheck

instance Arbitrary Char where
    arbitrary     = choose ('\32', '\128')
    coarbitrary c = variant (ord c `rem` 4)

讓我們啟動 GHCi 並嘗試一些通用屬性(可以直接從 Haskell REPL 使用 QuickCheck 測試框架很不錯)。首先是一個簡單的,[Char] 等於自身

*A> quickCheck ((\s -> s == s) :: [Char] -> Bool)
OK, passed 100 tests.

剛剛發生了什麼?QuickCheck 生成了 100 個隨機的 [Char] 值,並應用了我們的屬性,檢查所有情況下的結果是否為 True。QuickCheck *為我們生成了測試集*!

現在是一個更有趣的屬性:反轉兩次返回恆等式

*A> quickCheck ((\s -> (reverse.reverse) s == s) :: [Char] -> Bool)
OK, passed 100 tests.

太棒了!

測試 take5

[編輯 | 編輯原始碼]

使用 QuickCheck 進行測試的第一步是找出函式對所有輸入都為真的某些屬性。也就是說,我們需要找到*不變式*。

一個簡單的不變式可能是:

所以讓我們將其寫成 QuickCheck 屬性

\s -> length (take5 s) == 5

然後我們可以在 QuickCheck 中執行它

*A> quickCheck (\s -> length (take5 s) == 5)
Falsifiable, after 0 tests:
""

啊!QuickCheck 發現了我們的錯誤。如果輸入字串包含少於 5 個可過濾字元,則結果字串的長度不會超過 5 個字元。因此,讓我們稍微弱化一下屬性:

也就是說,take5 返回一個長度最多為 5 個字元的字串。讓我們測試一下

*A> quickCheck (\s -> length (take5 s) <= 5)
OK, passed 100 tests.

好!

另一個屬性

[編輯 | 編輯原始碼]

另一件事需要檢查的是是否返回了正確的字元。也就是說,對於所有返回的字元,這些字元都是集合 ['a','b','c','d','e'] 的成員。

我們可以將其指定為:

在 QuickCheck 中

*A> quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))
OK, passed 100 tests.

優秀。因此,我們可以對該函式既不會返回過長的字串也不會包含無效字元充滿信心。

覆蓋率

[編輯 | 編輯原始碼]

在測試 [Char] 時,使用預設 QuickCheck 配置的一個問題是:標準的 100 次測試對於我們的情況來說不夠。實際上,當使用提供的 Char 的 Arbitrary 例項時,QuickCheck 永遠不會生成長度超過 5 個字元的字串!我們可以確認這一點

*A> quickCheck (\s -> length (take5 s) < 5)
OK, passed 100 tests.

QuickCheck 浪費時間生成不同的 Char,而我們真正需要的是更長的字串。解決此問題的一種方法是修改 QuickCheck 的預設配置以進行更深入的測試

deepCheck p = check (defaultConfig { configMaxTest = 10000}) p

這指示系統在得出一切正常之前找到至少 10000 個測試用例。讓我們檢查它是否正在生成更長的字串

*A> deepCheck (\s -> length (take5 s) < 5)
Falsifiable, after 125 tests:
";:iD^*NNi~Y\\RegMob\DEL@krsx/=dcf7kub|EQi\DELD*"

我們可以使用 'verboseCheck' 鉤子檢查 QuickCheck 生成的測試資料。這裡,在整數列表上進行測試

*A> verboseCheck (\s -> length s < 5)
0: []
1: [0]
2: []
3: []
4: []
5: [1,2,1,1]
6: [2]
7: [-2,4,-4,0,0]
Falsifiable, after 7 tests:
[-2,4,-4,0,0]

關於 QuickCheck 的更多資訊

[編輯 | 編輯原始碼]

有時為測試提供示例比根據一般規則定義測試更容易。HUnit 提供了一個單元測試框架,可以幫助你做到這一點。你也可以濫用 QuickCheck,透過提供一個恰好適合你的示例的一般規則;但在這種情況下,直接使用 HUnit 可能工作量更少。

待辦事項:提供 HUnit 測試示例,並對其進行簡要介紹

有關使用 HUnit 的更多詳細資訊,請參閱其使用者指南



華夏公益教科書