How can I write a QuickCheck test case to test for this case?
You shouldn't! QuickCheck is a tool for property based testing. In property based testing, you give a property of your data structure (or whatever) and the testing tool will automatically generate test cases to see if that property holds for the generated test cases. So, let's see how you can give a property instead of giving concrete test cases like [1,2,3]
and why properties are advantageous!
So. I started off with
import Test.QuickCheck
import qualified Data.Set as Set
import Data.Set (Set)
data Profile = Profile (Set Int)
deriving (Eq, Show)
mkProfile :: [Int] -> Profile
mkProfile = Profile . Set.fromList
-- | We will test if the order of the arguments matter.
test_mkProfile :: [Int] -> Bool
test_mkProfile xs = (mkProfile xs `comp` mkProfile (reverse xs))
where comp | length xs <= 1 = (==)
| otherwise = (/=)
This is how I reasoned for my property: Well, for empty and singleton list case, then reverse
is just the identity, so we expect mkProfile xs
be the same as mkProfile (reverse xs)
. Right? I mean mkProfile
gets exactly the same argument. In the case that length xs >= 2
then reverse xs
clearly isn't xs
. Like reverse [1, 2] /= [2, 1]
. And we know that a Profile do care about the order.
Now lets try this out in ghci
*Main> quickCheck test_mkProfile
*** Failed! Falsifiable (after 3 tests and 1 shrink):
[0,0]
Note now that there actually are two mistakes in our code. One, First, Profile
should be using a list and not a set. Second, our property is wrong! Because even if length xs >= 2
, xs == reverse (xs)
can be true. Let's try to fix the first error and see how quickcheck will still point out the second flaw.
data Profile2 = Profile2 [Int]
deriving (Eq, Show)
mkProfile2 :: [Int] -> Profile2
mkProfile2 = Profile2
-- | We will test if the order of the arguments matter.
test_mkProfile2 :: [Int] -> Bool
test_mkProfile2 xs = (mkProfile2 xs `comp` mkProfile2 (reverse xs))
where comp | length xs <= 1 = (==)
| otherwise = (/=)
Remember, our code is correct now but our property flawed!
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
+++ OK, passed 100 tests.
*Main> quickCheck test_mkProfile2
*** Failed! Falsifiable (after 8 tests):
[-8,-8]
Yes. You still need to think! Or you might get the false impression that everything is ok just because your code literally passed 700 test cases! Ok now lets fix our property too!
test_mkProfile2_again :: [Int] -> Bool
test_mkProfile2_again xs = (mkProfile2 xs `comp` mkProfile2 ys)
where ys = reverse xs
comp | xs == ys = (==)
| otherwise = (/=)
Now lets see that it works multiple times!
*Main> import Control.Monad
*Main Control.Monad> forever $ quickCheck test_mkProfile2_again
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
+++ OK, passed 100 tests.
... (a lot of times)
Hooray. We've now not only squashed the bug in our Profile
implementation, but also got a much better understanding of our code and the properties it adheres too!
Profile
instead of an opaque type, builder function, and transformations. If you were given a proper ADT then you could simply decide on which properties should hold after each operation and test for those. – Thomas M. DuBuisson