Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using QuickCheck to test intentional error conditions

I've seen how QuickCheck can be used to test monadic and non-monadic code, but how can I use it to test code that handles errors, i.e., prints some message and then calls exitWith?

like image 216
Dan Avatar asked Sep 01 '13 18:09

Dan


2 Answers

A disclaimer first: I'm not an expert on QuickCheck and I had no experience with monadic checking before your question, but I see stackoverflow as an opportunity to learn new things. If there's an expert answer saying this can be done better, I'll remove mine.

Say you have a function test that can throw exceptions using exitWith. Here's how I think you can test it. The key function is protect, which catches the exception and converts it to something you can test against.

import System.Exit
import Test.QuickCheck
import Test.QuickCheck.Property
import Test.QuickCheck.Monadic

test :: Int -> IO Int
test n | n > 100   = do exitWith $ ExitFailure 1
       | otherwise = do print n
                        return n

purifyException :: (a -> IO b) -> a -> IO (Maybe b)
purifyException f x = protect (const Nothing) $ return . Just =<< f x

testProp :: Property
testProp = monadicIO $ do
  input <- pick arbitrary
  result <- run $ purifyException test $ input
  assert $ if input <= 100 then result == Just input
                           else result == Nothing

There are two disadvantages to this, as far as I can see, but I found no way over them.

  1. I found no way to extract the ExitCode exception from the AnException that protect can handle. Therefore, all exit codes are treated the same here (they are mapped to Nothing). I would have liked to have:

    purifyException :: (a -> IO b) -> a -> IO (Either a ExitCode)
    
  2. I found no way to test the I/O behavior of test. Suppose test was:

    test :: IO ()
    test = do
      n <- readLn
      if n > 100 then exitWith $ ExitFailure 1
                 else print n
    

    Then how would you test it?

I'd appreciate more expert answers too.

like image 174
nickie Avatar answered Sep 19 '22 17:09

nickie


The QuickCheck expectFailure function can be used to handle this type of thing. Take this simple (and not-recommended) error-handling framework:

import System.Exit
import Test.QuickCheck
import Test.QuickCheck.Monadic

handle :: Either a b -> IO b
handle (Left _)  = putStrLn "exception!" >> exitWith (ExitFailure 1)
handle (Right x) = return x

and whip up a couple of dummy functions:

positive :: Int -> Either String Int
positive x | x > 0     = Right x
           | otherwise = Left "not positive"

negative :: Int -> Either String Int
negative x | x < 0     = Right x
           | otherwise = Left "not negative"

Now we can test some properties of the error handling. First, Right values should not result in exceptions:

prop_returnsHandledProperly (Positive x) = monadicIO $ do
  noErr <- run $ handle (positive x)
  assert $ noErr == x

-- Main*> quickCheck prop_returnsHandledProperly
-- +++ OK, passed 100 tests.

Lefts should result in exceptions. Notice the expectFailure tacked on to the start:

prop_handlesExitProperly (Positive x) = expectFailure . monadicIO $
  run $ handle (negative x)

-- Main*> quickCheck prop_handlesExitProperly
-- +++ OK, failed as expected. Exception: 'exitWith: invalid argument (ExitFailure 0)' (after 1 test):
like image 23
jtobin Avatar answered Sep 18 '22 17:09

jtobin