Let's say you have a simple pure function that applies a discount of 30% if the total price of a list of Items 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]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.