Let's say you have a simple pure function that applies a discount of 30% if the total price of a list of Item
s is over 30.00 (let's not delve into the fact that I'm using a Float
to indicate the price):
promoGet30PercentOff :: [Item] -> Float
promoGet30PercentOff xs
| total > 30.0 = total * 0.7
| otherwise = total
where total = sum $ map itemPrice xs
and Item
and itemPrice
are defined as follows:
data Item = Apple | Banana deriving Show
itemPrice :: Item -> Float
itemPrice x =
case x of
Apple -> 5.9
Banana -> 3.0
I'd now like to test promoGet30PercentOff
at the boundary.
As I come from Python, my natural reaction would be to mock Item
and create some that are priced at 30.0, 30.1 and 0.0, for instance.
I believe, this is not possible in Haskell (but correct me if I'm wrong). How would you go about it then?
1 Answer 1
You can untangle your promo function from the Item
data type
promoGet30PercentOffWith :: (a -> Float) -> [a] -> Float
promoGet30PercentOffWith price xs
| total > 30.0 = total * 0.7
| otherwise = total
where total = sum $ map price xs
promoGet30PercentOff :: [Item] -> Float
promoGet30PercentOff = promoGet30PercentOffWith itemPrice
Because promoGet30PercentOffWith
is polymorphic, there is a free theorem that all functions of its type satisfy:
promoGet30PercentOffWith f xs = promoGet30PercentOffWith id (map f xs)
This means that the general behavior of that function is entirely determined by its specialized behavior when the first argument is id
---so the list argument has type [Float]
. Thus, mocking items with artificial prices is as simple as giving the list of prices to the function.
shouldPromo :: [Float] -> IO ()
shouldPromo xs = promoGet30PercentOffWith id xs === sum xs * 0.7
shouldNotPromo :: [Float] -> IO ()
shouldNotPromo xs = promoGet30PercentOffWith id xs === sum xs
(===) :: Eq a => a -> a -> IO ()
x === y = if x == y then return () else error "assertion failed"
test :: IO ()
test = do
shouldPromo [30.1]
shouldNotPromo [30.0]
shouldNotPromo [0]