I'm trying to get started with Haskell's QuickCheck, and while I am familiar with the concepts behind the testing methodology, this is the first time I am trying to put it to use on a project that goes beyond testing stuff like reverse . reverse == id
and that kind of thing. I want to know if it is useful to apply it to business logic (I think it very much could be).
So a couple of existing business logic type functions that I would like to test look like the following:
shouldDiscountProduct :: User -> Product -> Bool
shouldDiscountProduct user product =
if M.isNothing (userDiscountCode user)
then False
else if (productDiscount product) then True
else False
For this function I can write a QuickCheck spec like the following:
data ShouldDiscountProductParams
= ShouldDiscountProductParams User Product
instance Show ShouldDiscountProductParams where
show (ShouldDiscountProductParams u p) =
"ShouldDiscountProductParams:\n\n" <>
"- " <> show u <> "\n\n" <>
"- " <> show p
instance Arbitrary ShouldDiscountProductParams where
arbitrary = ShouldDiscountProductParams <$> arbitrary <*> arbitrary
shouldDiscountProduct :: Spec
shouldDiscountProduct = it behavior (property verify)
where
behavior =
"when product elegible for discount\n"
<> " and user has discount code"
verify (ShouldDiscountProductParams p t) =
subject p t `shouldBe` expectation p t
subject =
SUT.shouldDiscountProduct
expectation User{..} Product{..} =
case (userDiscountCode, productDiscount) of
(Just _, Just _) -> True
_ -> False
And what I end up with is a function expectation
that verifies the current implementation of shouldDiscountProduct
, just more elegantly. So now I have a test, I can refactor my original function. But my natural inclination would be to change it to the implementation in expectation
:
shouldDiscountProduct User{..} Product{..} =
case (userDiscountCode, productDiscount) of
(Just _, Just _) -> True
_ -> False
But this is fine right? If I want to change this function again in future I have the same function ready to verify my changes are appropriate and not inadvertently breaking something.
Or is this overkill / double bookkeeping? I suppose I have had ingrained into me from OOP testing that you should try and avoid mirroring the implementation details as much as possible, this literally couldn't be any further than that, it is the implementation!
I then think as I go through my project and add these kinds of tests, I am effectively going to be addding these tests, and then refactoring to the cleaner implementation I implement in the expectation
assertion. Obviously this isn't going to be the case for more complex functions than these, but in the round I think will be the case.
What are people experiences with using property based testing for business logic-type functions? Are there any good resources out there for this kind of thing? I guess I just want to verify that I am using QC in an appropriate way, and its just my OOP past throwing doubts in my mind about this...