Haskellでの BDD を実践するとどうなるかを考えるためのメモ。
型
豊かなデータ型とセクシーな型システムを持つ Haskell では、型が以下のような意味を持つ。
- 仕様
- 保守性の向上
- 簡単なドキュメント
- 設計図
BDD では、テストの用語ではなく設計の用語を使ってテストを記述する。だから Haskell で、まず型を書く習慣があれば、ある意味 BDD を実践していると言える。この感覚は、他の言語のプログラマには分からないかもしれない。
fromList :: Ord a => [a] -> Set a fromList = undefined
このコードはコンパイルを通過するので、型に関する誤りがないことを確かめられる。
僕はへなちょこなので、型を先に書くこともあれば、後から書くこともある。
値
型に関する仕様が書けたら、次は値に関する仕様を書くべきだ。依存型とかを使えば、それ以上のことができるという意見もあるだろうけど、とっつきにくいので今は考えない。
Haskell には、Ruby の rspec をまねた hspec があり一部の人は絶賛している。rspec のチュートリアルを読んでみて、気持ちはわかったのだけれど、僕にはピンとこなかった。
結局 rspec は、仕様書だと思ってテストコードをコードとは別のファイルに書く訳で、それがドキュメントに反映されない。(反映するツールはあるのかもしれないけれど。)
それよりも、Python の doctest の方が、僕としてはいいと思う。Haskell には Python をまねた doctestがある。
{-| Creating a set from a list. O(N log N) >>> empty == fromList [] True >>> singleton 'a' == fromList ['a'] True >>> fromList [5,3,5] == fromList [5,3] True -} fromList :: Ord a => [a] -> Set a fromList = undefined
このように対話的な使用例を書く。この書式には haddock が対応しているので、doctest は haddock から使用例のデータをもらって、対話的に実行し検査する。BBD 風にするには、それぞれの使用例にもう少し説明を加えるべきかもしれないと思う一方で、これで十分な気もする。
でも、まだいろいろ不満がある。
- test-framework と統合されていない
- 対話的な例ではなく、等式だけを書きたい
- QuickCheck のプロパティが書けない
test-framework
test-framework のドライバは、HUnit 用、QuickCheck2 用、doctest 用が用意されている。test-framework-th を使えば、HUnit のユニットテストと QuickCheck2 のプロパティを書くのが簡単になる。それぞれ、"case_" と "prop_" で始まる関数を書けばいい。
module Main where import Test.Framework.TH import Test.Framework.Providers.HUnit import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck2 import Test.HUnit import Data.MySet main :: IO () main = $(defaultMainGenerator) prop_toList :: [Int] -> Bool prop_toList xs = ordered ys where ys = toList . fromList $ xs ordered (x:y:xys) = x <= y && ordered (y:xys) ordered _ = True case_ticket4242 :: Assertion case_ticket4242 = (valid $ deleteMin $ deleteMin $ fromList [0,2,5,1,6,4,8,9,7,11,10,3]) @?= True
defaultMain に与える Test のリストが Template Haskell で自動生成され、すごく便利だ。しかし、doctest が統合されていないので、test-framework-th-prime を作成してみた。手伝ってくれた @mr_konn さん、ありがとう!
module Main where import Test.Framework.TH.Prime import Test.Framework.Providers.DocTest import Test.Framework.Providers.HUnit import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck2 import Test.HUnit import Data.MySet main :: IO () main = $(defaultMainGenerator) doc_test :: DocTests doc_test = docTest ["../Data/MySet.hs"] ["-i.."] prop_toList :: [Int] -> Bool prop_toList xs = ordered . toList . fromList $ xs where ordered (x:y:xys) = x <= y && ordered (y:xys) ordered _ = True case_ticket4242 :: Assertion case_ticket4242 = (valid $ deleteMin $ deleteMin $ fromList [0,2,5,1,6,4,8,9,7,11,10,3]) @?= True
なお、Mac 上の GHC 7.0 でこのコードを実行するにはコンパイルする必要がある。runghc などで動かすと segfault する。GHC 7.4 では直るようだ(未確認)。
等式
対話的な例よりも、等式の方がしっくりくる場合がある。
{-| Creating a set from a list. O(N log N) >>> empty == fromList [] >>> singleton 'a' == fromList ['a'] >>> fromList [5,3,5] == fromList [5,3] -} fromList :: Ord a => [a] -> Set a fromList = undefined
こうなると、こういう具体例だけではなく、QuickCheck のプロパティ、つまり関数の持つ性質を記述したくなる
QuickCheck
Simon M さんは、すでに haddock に対し、"prop>"という新しい書式を導入することに賛成しているそうだ。加えて "unit> という新しい書式を導入し対話的な例と区別することにすると、こんな感じになるだろうか?
{-| Creating a set from a list. O(N log N) prop> empty == fromList [] unit> singleton 'a' == fromList ['a'] prop> singleton (x :: Char) == fromList [x] unit> fromList [5,3,5] == fromList [5,3] prop> fromList (xs :: [Int]) == fromList (uniq xs) prop> ordered . toList . fromList $ (xs :: [Int]) -} fromList :: Ord a => [a] -> Set a fromList = undefined
test-framework のテストコードに書いているコードの多くはドキュメントに移動することになる。こんな環境が整ったら、テストコードには以下を記述することになるだろう。
考察
"prop>" と "unit>" は統合できるのか否か? 乱数的な変数のないプロパティを QuickCheck に与えると、1回しか実行しなくていいのに、100 回実行してしまう問題がある。
プロパティに現れる自由変数はどうする?無名関数を書かせて、閉じたλ式にさせる?
prop> \(xs :: [Int]) -. ordered . toList . fromList $ xs
そもそも、テストコードは test-framework の延長ではなく、hspec を発展させる方がいいのではないかという意見もあるかもしれない。そういう意見の方は、ぜひ僕を納得させてほしい。