読者です 読者をやめる 読者になる 読者になる

taketoncheir.log

Like the Decatoncheir by Poseidon Industrial, This blog is Yet Another Storage for My Long Term Memories.

QuickCheckでデータ型に対するテストを書く

Haskell QuickCheck

QuickCheckで自分で定義したデータ型のテストを書く

QuickCheckです。
QuickCheckはIntやStringについてデータを生成はしてくれますが、自分で定義したデータ型についてはそのままでは生成してくれません。

データ型に対してArbitraryのinstanceを定義すればよい

例えば、以下のようなデータ型を定義したとする。

data Person = Person
	{ name :: Text
	, age :: Int
	, sex :: Sex
	}
  deriving (Show, Eq)

data Sex = Male | Female
  deriving (Show, Eq)

Personというデータ型について、name, age, sexというフィールドがあると。
sexはSex型で、値としてMaleかFemaleがありえます。

QuickCheckでこのPersonの値をランダムに生成したい場合、以下のようになります。

import Control.Monad
import Control.Applicative
import Test.QuickCheck
import qualified Data.Text as DT

instance Arbitrary Person where
    arbitrary = return Person 
        `ap` (DT.pack <$> arbitrary)
        `ap` arbitrary
        `ap` arbitrary

instance Arbitrary Sex where
    arbitrary = elements [Male, Female]

Personの各フィールドに対応したarbitraryが呼ばれます。
一つ目のarbitraryでは、instance Arbitrary Stringでの定義が、二つ目のarbitraryでは、instance Arbitrary Intでの定義が呼ばれます。
三つ目はinstance Arbitrary Sexでの定義が呼ばれます。
ここでは、MaleかFemaleを要素としてどちらかを生成するelements関数を用いています。

以上で実行すると、Personのフィールドにランダムな値が入ったデータが返されると。

プロパティに与える際にデータをフィルタリングしたい場合は、forAll関数等を用いて条件をセットしてやります。

-- Maleだけ通す
genMales :: Gen [Person]
genMales = (filter (\x -> personSex x == Male)) <$> arbitrary

-- Maleばかり与えられたら、Femaleの合計は0のはず
prop_calcNumPerAge_allMales :: [Person] -> Property
prop_calcNumPerAge_allMales xs =
    forAll genMales $ \xs ->
    collect (length xs) $
        (sumFemales $ calculateNumPerAge xs) == 0
  where
    sumFemales :: AllNumPerAge -> Int
    sumFemales x = (over0Female x)
                + (over20Female x)
                + (over40Female x)
                + (over60Female x)

以上のコードはこちらにあります。
QuickCheck、簡単ですねぇ。