Unit Testing with HUNit

Table of Contents

  • Index
  • Repository
  • 1 Unit Testing with HUnit

    1.1 Overview - HUnit functions and types

    The package HUnit provides unit testing facilities for Haskell. It is inspired by JUnit tool for Java.

    Documentation: HUnit: A unit testing framework for Haskell

    Install HUnit

    $ stack install HUnit
    

    Assert Functions

    Function   Signature
         
    assert :: Assertable t => t -> Assertion
    assertFailure :: assertFailure :: String -> Assertion
    assertBool :: String -> Bool -> Assertion
    assertEqual :: (Show a, Eq a) => String -> a -> a -> Assertion
    assertionPredicate :: AssertionPredicable t => t -> AssertionPredicate
    assertString :: String -> Assertion
         

    Types

    Type Assertion

    type Assertion = IO ()
    

    Type: Count

    data Counts = Counts { cases, tried, errors, failures :: Int }
                  deriving (Eq, Show, Read)
    

    Type: Test

    data Test = TestCase Assertion
              | TestList [Test]
              | TestLabel String Test
    

    Functions

    assertFailure displays an error message.

    assertFailure :: String -> Assertion
    assertFailure msg = ioError (userError ("HUnit:" ++ msg))
    

    Example:

    > assertFailure "It is going to fail. Fatal Kernel error ... error code 0x42343" 
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 4, locationColumn = 1})) 
    "It is going to fail. Fatal Kernel error ... error code 0x42343"
    > 
    >
    

    assertBool shows an error message if the condition fails.

    assertBool :: String -> Bool -> Assertion
    assertBool msg b = unless b (assertFailure msg)
    

    Example:

    > assertBool "It should be true" True
    > assertBool "It should be true" False
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 10, locationColumn = 1})) 
    "It should be true"
    >
    

    assertString shows an error message if the the input string is not empty.

    assertString :: String -> Assertion
    assertString s = unless (null s) (assertFailure s)
    

    Example:

    > assertString "hello world"
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 19, locationColumn = 1}))
    "hello world"
    > 
    
    > assertString ""
    >
    

    assertEqual shows an error message if the input values are not equal.

    assertEqual :: (Eq a, Show a) => String -> a -> a -> Assertion
    assertEqual preface expected actual =
      unless (actual == expected) (assertFailure msg)
     where msg = (if null preface then "" else preface ++ "\n") ++
                 "expected: " ++ show expected ++ "\n but got: " ++ show actual
    

    Example:

    > assertEqual "10 + 15 should be equal to 25" 25 (10 + 15)
    > 
    > assertEqual "10 + 15 should be equal to 35" 35 (10 + 15)
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 17, locationColumn = 1}))
    "10 + 15 should be equal to 35\nexpected: 35\n but got: 25"
    >
    

    Run the test cases

    > :t runTestTT
    runTestTT :: Test -> IO Counts
    

    Type Test

    data Test = TestCase Assertion
              | TestList [Test]
              | TestLabel String Test
    

    Example: Running a single test

    > runTestTT (TestCase $ assertEqual "sum should be equal to 25" 25 (10 + 15))
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    > 
    > runTestTT (TestCase $ assertEqual "sum should be equal to 25" 25 (50 + 15))
    ### Failure:                              
    <interactive>:161
    sum should be equal to 25
    expected: 25
     but got: 65
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    >
    

    Example: Running a single test with label

    > let testEqual25 = TestLabel "testEqual25" (TestCase $ assertEqual "sum should be equal to 25" 25 (10 + 15))
    > 
    
    > runTestTT testEqual25 
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    > 
    
    > let testEqual25_2 = TestLabel "testEqual25" (TestCase $ assertEqual "sum should be equal to 25" 25 (20 + 15))
    > 
    > testEqual25_2 
    TestLabel testEqual25 TestCase _
    > 
    > runTestTT testEqual25_2 
    ### Failure in: testEqual25               
    <interactive>:153
    sum should be equal to 25
    expected: 25
     but got: 35
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    > 
    >
    

    Example: Running multiple test cases.

    > let testTrue = TestCase $ assertBool "It should be true" True
    > let testFalse = TestCase $ assertBool "It is gonna fail." False 
    > let testEqual25 = TestCase $ assertEqual "The sum should be equal to 25" 25 (10 + 15)
    > let testEqual25Fail = TestCase $ assertEqual "The sum should be equal to 25" 25 (150 + 25)
    > 
    
    > runTestTT $ TestList [testTrue, testFalse, testEqual25, testEqual25Fail]
    ### Failure in: 1                         
    <interactive>:164
    It is gonna fail.
    ### Failure in: 3                         
    <interactive>:166
    The sum should be equal to 25
    expected: 25
     but got: 175
    Cases: 4  Tried: 4  Errors: 0  Failures: 2
    Counts {cases = 4, tried = 4, errors = 0, failures = 2}
    > 
    
    :{
    runTestTT $ TestList [TestLabel "testTrue" testTrue,
                          TestLabel "testFalse" testFalse,
                          TestLabel "testEqual25" testEqual25,
                          TestLabel "testEqual25Fail" testEqual25Fail
                         ]
    :}
    
    -- Pasting this code in the terminal:
    > :{
    - runTestTT $ TestList [TestLabel "testTrue" testTrue,
    -                       TestLabel "testFalse" testFalse,
    -                       TestLabel "testEqual25" testEqual25,
    -                       TestLabel "testEqual25Fail" testEqual25Fail
    -                      ]
    - :}
    ### Failure in: 1:testFalse               
    <interactive>:164
    It is gonna fail.
    
    ### Failure in: 3:testEqual25Fail         
    <interactive>:166
    The sum should be equal to 25
    expected: 25
     but got: 175
    Cases: 4  Tried: 4  Errors: 0  Failures: 2
    Counts {cases = 4, tried = 4, errors = 0, failures = 2}
    >
    

    Operators

    Description Operator   Signature
    Assert Bool (True) (@?) :: (AssertionPredicable t) => t -> String -> Assertion
           
    Assert Equal (@=?) :: (Show a, Eq a) => a: expected -> a: value -> Assertion
    Assert Equal (@?=) :: (Eq a, Show a) => a: value -> a: expected -> Assertion
           
           
    Creates test case with label (~:) :: Testable t => String -> t -> Test
           
    Creates an equality test case (~?=) :: (Show a, Eq a) => a: value -> a: expected -> Test
    Creates an equality test case (~=?) :: (Show a, Eq a) => a: expected -> a: value -> Test
           

    Example: (@=?)

    > :t (@=?)
    (@=?) :: (Show a, Eq a) => a -> a -> Assertion
    >
    
    > 10 @=? (1 + 2 + 3 + 4)
    > 10 @=? (1 + 2 + 3 + 4 + 5)
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 46, locationColumn = 1})) "expected: 10\n but got: 15"
    > 
    
    > 120 @=? product [1, 2, 3, 4, 5]
    > 120 @=? product [1, 2, 3, 4, 5, 6]
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 58, locationColumn = 1})) "expected: 120\n but got: 720"
    >
    

    Example: (@?)

    > 100 > 10 @? "It should be true" 
    
    > 100 < 10 @? "It should be true" 
     *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 286, locationColumn = 1})) "It should be true"
    >
    

    Example: (~:)

    > :t (~:)
    (~:) :: Testable t => String -> t -> Test
    > 
    
    > let test1 = "testSumIs25" ~: assertEqual "The sum is 25" 25 (10 + 15)
    > runTestTT test1
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    > 
    
    > let test2 = "testSumIs25" ~: assertEqual "The sum is 25" 25 (13 + 15)
    > runTestTT test2
    ### Failure in: testSumIs25               
    <interactive>:223
    The sum is 25
    expected: 25
     but got: 28
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    > 
    
    > let test3 = "testSumIs25" ~:  25 @=? (10 + 15)
    > runTestTT test3
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    > 
    
    > let test4 = "testSumIs25" ~:  25 @=? (1023 + 15)
    > runTestTT test4
    ### Failure in: testSumIs25               
    <interactive>:234
    expected: 25
     but got: 1038
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    >
    

    Example: (~?=)

    > runTestTT $ 20 ~?= 20
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    > 
    
    > runTestTT $ 10 ~?= 20
    ### Failure:                              
    <interactive>:276
    expected: 20
     but got: 10
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    >
    

    Example: (~=?)

    > runTestTT $ 10 ~=? 20
    ### Failure:                              
    <interactive>:281
    expected: 10
     but got: 20
    Cases: 1  Tried: 1  Errors: 0  Failures: 1
    Counts {cases = 1, tried = 1, errors = 0, failures = 1}
    > 
    
    > runTestTT $ 20 ~=? 20
    Cases: 1  Tried: 1  Errors: 0  Failures: 0
    Counts {cases = 1, tried = 1, errors = 0, failures = 0}
    >
    
    import Test.HUnit
    import System.IO
    
    :{
    fact 1 = 1
    fact n = n * fact (n - 1)
    :}
    
    :{
    tests = TestList
        [ "fact 1" ~: fact 1 ~?= 1
        , "fact 2" ~: fact 2 ~?= 2
        , "fact 3" ~: fact 3 ~?= 6
        , "fact 4" ~: fact 4 ~?= 24
        , "fact 5" ~: fact 5 ~?= 120
        ]
    :}
    
    > runTestTT tests
    Cases: 5  Tried: 5  Errors: 0  Failures: 0
    Counts {cases = 5, tried = 5, errors = 0, failures = 0}
    >
    

    1.2 HUnit in the REPL

    Example using $ stack ghci

    > import Test.HUnit
    > 
    
    > :info Counts
    data Counts
      = Counts {cases :: Int,
                tried :: Int,
                errors :: Int,
                failures :: Int}
            -- Defined in ‘Test.HUnit.Base’
    instance [safe] Eq Counts -- Defined in ‘Test.HUnit.Base’
    instance [safe] Read Counts -- Defined in ‘Test.HUnit.Base’
    instance [safe] Show Counts -- Defined in ‘Test.HUnit.Base’
    > 
    
    > :info TestCase
    data Test = TestCase Assertion | ...
            -- Defined in ‘Test.HUnit.Base’
    
    > :t TestCase 
    TestCase :: Assertion -> Test
    
    
    > :info Assertion
    type Assertion = IO ()  -- Defined in ‘Test.HUnit.Lang’
    > 
    
    > 
    > :{
    - addInt :: Int -> Int -> Int 
    - addInt x y = x + y
    - :}
    > 
    > addInt 10 20
    30
    > 
    > import Test.HUnit
    > 
    > :t TestCase
    TestCase :: Assertion -> Test
    > 
    > assertEqual "add 2 10 should be 12" 12 (add 10 2)
    > 
    > assertEqual "add 2 10 should be 12" 0 (add 10 2)
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 23, locationColumn = 1})) 
    "add 2 10 should be 12\nexpected: 0.0\n but got: 12.0"
    >
    
    > assertBool "It should be true" True
    > assertBool "It should be true" False
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 50, locationColumn = 1})) 
    "It should be true"
    > 
    >
    
    > assertBool "It should be true" (10 > 5)
    > assertBool "It should be true" (10 < 5)
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 54, locationColumn = 1})) 
    "It should be true"
    > 
    
    
    :{
    testFile filename msg = do 
      writeFile filename msg
      content <- readFile filename
      assertEqual ("File content should be equal to \"" ++ msg ++ "\"") msg content
    :}
    
    > testFile "/tmp/testfile1" "hello world"
    > 
    
    :{
    testFile2 filename msg msgExpect = do 
      writeFile filename msg
      content <- readFile filename
      assertEqual ("File content should be equal to \"" ++ msg ++ "\"") msgExpect content
    :}
    
    > testFile2 "/tmp/testfile2" "hello world" "hello world"
    
    > testFile2 "/tmp/testfile2" "hello world" "hello"
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 90, locationColumn = 3})) 
    "File content should be equal to \"hello world\"\nexpected: \"hello\"\n but got: \"hello world\""
    >
    
    > testFile2 "/afile.txt" "hello" "hello"
     *** Exception: /afile.txt: openFile: permission denied (Permission denied)
    > 
    
    > runTestTT $ TestCase $ testFile2 "/afile.txt" "hello" "hello"
    ### Error:                                
    /afile.txt: openFile: permission denied (Permission denied)
    Cases: 1  Tried: 1  Errors: 1  Failures: 0
    Counts {cases = 1, tried = 1, errors = 1, failures = 0}
    > 
    
    
    
    > import Text.Read (readMaybe)
    >  
    > assertEqual "The parsed string should be equal to 100" (Just 100) ((readMaybe "100") :: Maybe Int)
    > 
    > assertEqual "The parsed string should be equal to 100" (Just 100) ((readMaybe "1asd00") :: Maybe Int)
    *** Exception: HUnitFailure (Just (Location {locationFile = "<interactive>", locationLine = 115, locationColumn = 1})) 
    "The parsed string should be equal to 100\nexpected: Just 100\n but got: Nothing"
    > 
    
    
    
    > let testSum = TestCase $ assertEqual "10 + 5 = 15" 15 (10 + 5)
    > let testProd = TestCase $ assertEqual "10 * 15" 150 (10 * 15)
    > let testPred = TestCase $ assertBool "10 > 5" (10 > 5)
    >
    
    > let testFailure = TestCase $ assertEqual "It will fail 10 + 2 = 15" (10 + 2) 15  
    
    :{  
    testlist = TestList [TestLabel "testSum" testSum,
                         TestLabel "testPred" testPred,
                         TestLabel "testFailure" testFailure,
                         TestLabel "testProd" testProd                    
                        ]
    
    :}
    
    > runTestTT testlist
    ### Failure in: 2:testFailure             
    <interactive>:94
    It will fail 10 + 2 = 15
    expected: 12
     but got: 15
    Cases: 4  Tried: 4  Errors: 0  Failures: 1
    Counts {cases = 4, tried = 4, errors = 0, failures = 1}
    > 
    
    :{  
    testlist2 = TestList [TestLabel "testSum" testSum,
                          TestLabel "testPred" testPred,
                          -- TestLabel "testFailure" testFailure,
                          TestLabel "testProd" testProd                    
                         ]
    
    :}  
    
    
    > runTestTT testlist2
    Cases: 3  Tried: 3  Errors: 0  Failures: 0
    Counts {cases = 3, tried = 3, errors = 0, failures = 0}
    >
    

    1.3 Example: Unit test with HUnit

    -- File: unitTestExample.hs
    
    import Test.HUnit
    
    testSum = TestCase $ assertEqual "10 + 5 = 15" 15 (10 + 5)
    
    testProd = TestCase $ assertEqual "10 * 15" 150 (10 * 15)
    
    testPred = TestCase $ assertBool "10 > 5" (10 > 5)
    
    testFailure = TestCase $ assertEqual "It will fail 10 + 2 = 15" (10 + 2) 15  
    
    testlist = TestList [TestLabel "testSum" testSum,
                         TestLabel "testPred" testPred,
                         TestLabel "testFailure" testFailure,
                         TestLabel "testProd" testProd                    
                        ]
    
    main :: IO ()
    main = do
      runTestTT testlist
      return ()
    

    Running the tests

    Run the test as a script with runhaskell:

    $ stack exec -- runhaskell /tmp/uniTestExample.hs 
    ### Failure in: 2:testFailure             
    /tmp/uniTestExample.hs:12
    It will fail 10 + 2 = 15
    expected: 12
     but got: 15
    Cases: 4  Tried: 4  Errors: 0  Failures: 1
    

    Run the test compiling:

    $ stack exec -- ghc --make /tmp/uniTestExample.hs 
    [1 of 1] Compiling Main             ( /tmp/uniTestExample.hs, /tmp/uniTestExample.o )
    Linking /tmp/uniTestExample ..
    
    $ /tmp/uniTestExample
    ### Failure in: 2:testFailure             
    /tmp/uniTestExample.hs:12
    It will fail 10 + 2 = 15
    expected: 12
     but got: 15
    Cases: 4  Tried: 4  Errors: 0  Failures: 1
    

    Author: nobody

    Created: 2018-06-17 Sun 02:37

    Emacs 25.3.1 (Org mode 8.2.10)

    Validate