HaskellのDocTestでTDDしてみる+α
(注意)この記事はお試しでやってみたよ的な記事です。
Haskell界隈で面白そうな話をみた。
『プログラミング Haskell』『すごい Haskell 楽しく学ぼう!』の次に何を読む? - Togetterまとめ
こういう話を見ていると自分でも試したくなりますよね。
というわけで、例によってFizzBuzzをお題にしてやってみました。まぁ、Haskellはじめて間もないのでこれくらいのお題がちょうどよさそうですし・・・コードひどかったりしたらご指摘ください。
環境準備
どうせだからディレクトリ構成からきちんと考えたいですよね、ということでGItHubに置かれているHaskellライブラリのディレクトリ構成を参考に作ってみました。
fizzbuzz |- Setup.hs |- fizzbuzz.cabal |- src |- FizzBuzz.hs |- dist |- ビルドやテストでの生成物置き場 |- test |- doctests.hs |- Spec.hs |- Tests.hs
fizzbuzz.cabalはcabal initコマンドで基本情報を埋めたあと、以下のように書き加えました。
name: fizzbuzz -- (省略) build-type: Custom library hs-source-dirs: src ghc-options: -Wall build-depends: base exposed-modules: FizzBuzz test-suite doctests type: exitcode-stdio-1.0 hs-source-dirs: test main-is: doctests.hs ghc-options: -Wall -threaded build-depends: base, doctest >= 0.7 test-suite spec type: exitcode-stdio-1.0 hs-source-dirs: test, src main-is: Spec.hs ghc-options: -Wall build-depends: base, hspec, HUnit test-suite tests type: exitcode-stdio-1.0 hs-source-dirs: test, src main-is: Tests.hs ghc-options: -Wall build-depends: base, test-framework, test-framework-th, test-framework-quickcheck2, test-framework-hunit, HUnit, QuickCheck >= 2.4.0.1
各ライブラリのバージョン指定はもう少ししっかり書くべきかもしれません・・・。
doctests.hsの作成
こちらを参考にしました。
module Main where import Test.DocTest main :: IO () main = doctest ["--optghc=-isrc", "src/FizzBuzz.hs"]
[2012/7/25 追記]
srcディレクトリ下の他のソースファイルもサーチしてほしい場合もあるので、"--optghc=-isrc"を追加
(-isrcというオプションではなく、-iオプションにsrcを指定している)
TDDしてみる
FIzzBuzz.hsを作成し、テストを書きます。今回はhaddockとdoctestで試してみました。
まず3の倍数は"Fizz"を返す、からいきます。
module FizzBuzz (fizzbuzz) where {-| function 'fizzbuzz'. >>> fizzbuzz 3 "Fizz" -} fizzbuzz x = undefined
今回は後から型を書くことにしました。
出来上がったのでビルドとテストを実行します。
cabal configure --enable-test cabal build cabal test doctests
もちろん失敗します。失敗したので実装しましょう(これ以降、実装時は関数のみ書きます)。
fizzbuzz x = "Fizz"
仮実装でとりあえずテストに通りました。が、これはひどいので三角測量で更にテストを追加します。
module FizzBuzz (fizzbuzz) where {-| function 'fizzbuzz'. >>> fizzbuzz 3 "Fizz" >>> fizzbuzz 6 "Fizz" -} fizzbuzz x = "Fizz"
テストは通らなくなります。まじめに実装しましょう。
fizzbuzz x | x `mod` 3 == 0 = "Fizz"
大丈夫そうですね。
次は5の倍数について・・・となるのですが、長いのでこの記事では省略します。
途中経過
さて、続けていった結果以下の状態になりました。
module FizzBuzz (fizzbuzz) where {-| function 'fizzbuzz'. 'fizzbuzz'は与えられた整数に対応する文字列を返す。 例: 3の倍数 >>> fizzbuzz 3 "Fizz" 5の倍数 >>> fizzbuzz 5 "Buzz" 3かつ5の倍数 >>> fizzbuzz 15 "FizzBuzz" それ以外 >>> fizzbuzz 1 "1" -} fizzbuzz x | multiples3 && multples5 = "FizzBuzz" | multiples3 = "Fizz" | multiples5 = "Buzz" | otherwise = show x where multiples3 = x `mod` 3 == 0 multiples5 = x `mod` 5 == 0
私の環境だと日本語を認識できたのでとりあえず日本語で書いてます。あと、三角測量に使用したテスト(6なら"Fizz"とか)は、ずっと必要なテストではないと考えたので途中で削りました。ドキュメントなので最小構成でいいかなと思ったというのもあります。
doctestを使った感覚としては「楽でいいなー」といったところでしょうか。
IO系は厳しいものの、副作用のないものであればこれで十分な感じもありますね。とはいっても、ドキュメントとしては書きたくないけどテストとして残しておきたいコードはどうするのという話もありますが・・・と思っていたらHaskellとテストとBDD - あどけない話に色々と書かれていました。非常に参考になります。
番外編
ひと段落してからQuickCheckのコードも作ってみました。
{-# LANGUAGE TemplateHaskell #-} module Main where import Test.Framework.TH import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck2 import Control.Monad (liftM) import FizzBuzz main :: IO () main = $(defaultMainGenerator) prop_fizz :: Int -> Property prop_fizz x = (pred_mod3 x) && (x > 0) ==> fizzbuzz x == "Fizz" prop_buzz :: Property prop_buzz = forAll (liftM abs arbitrary) $ \x -> (pred_mod5 x) ==> fizzbuzz x == "Buzz" prop_fizzbuzz :: Property forAll (liftM ((*15) . abs) arbitrary) $ \x -> (pred_both x) ==> fizzbuzz x == "FizzBuzz" prop_non :: Int -> Property prop_non x = (pred_non x) && (x > 0) ==> fizzbuzz x == (show x) pred_non :: Int -> Bool pred_non x = (mod3 x > 0) && (mod5 x > 0) pred_mod3 :: Int -> Bool pred_mod3 x = (mod3 x == 0) && (mod5 x > 0) pred_mod5 :: Int -> Bool pred_mod5 x = (mod3 x > 0) && (mod5 x == 0) pred_both :: Int -> Bool pred_both x = (mod3 x == 0) && (mod5 x == 0) mod3 :: Int -> Int mod3 x = x `mod` 3 mod5 :: Int -> Int mod5 x = x `mod` 5
負の数を対象にしないように(x > 0)や(liftM abs arbitrary)してます。また、(liftM ((*15) . abs) arbitrary)で15の倍数にマッチするテストデータを増やしてます。
慣れていないのでそこそこ時間がかかりました。
cabal test tests
このコマンドで実行させました。.cabalに記述したtest-suiteの名前を指定することで実行したいtest-suiteを選べるようですね。(何も書かないとtestsが走る?用調査)
-
- 番外編その2
Hspecでも書いてみました。ただ、doctestの後だとどうも重い感じがしますね。
module Main where import Test.Hspec.Monadic import Test.Hspec.HUnit () import Test.HUnit import FizzBuzz main :: IO () main = hspecX spec spec :: Specs spec = do describe "fizzbuzz" $ do it "3の倍数ならFizz" $ do fizzbuzz 3 @?= "Fizz" it "5の倍数ならBuzz" $ do fizzbuzz 5 @?= "Buzz" it "3かつ5の倍数ならFizzBuzz" $ do fizzbuzz 15 @?= "FizzBuzz" it "その他数値は文字列化" $ do fizzbuzz 1 @?= "1"
いつもの(?)Specですね。
cabal test spec
これでテスト実行しました。
番外編3
最初はtest-framework-doctestを使おうとしていたのですが、deprecatedという単語が見えたので引き返しました。