Testing Haskell code is an important practice to ensure the correctness and quality of your programs. There are several libraries available to help you write and organize tests for your Haskell code. In this answer, I will cover three popular testing libraries: HUnit, QuickCheck, and Tasty.
1. HUnit
HUnit is a unit testing framework for Haskell, inspired by JUnit for Java. It provides a simple way to create test cases and assert that your functions return the expected results.
To use HUnit, you’ll first need to add it as a dependency in your ‘.cabal‘ or ‘package.yaml‘ file:
dependencies:
- HUnit
Next, you can import the ‘Test.HUnit‘ module and write test cases using the ‘TestCase‘ constructor along with assert functions such as ‘assertEqual‘ and ‘assertBool‘.
Here’s an example testing a simple ‘add‘ function:
module Main where
import Test.HUnit
add :: Int -> Int -> Int
add x y = x + y
test1 = TestCase (assertEqual "2 + 2 should equal 4" 4 (add 2 2))
test2 = TestCase (assertEqual "3 + 4 should equal 7" 7 (add 3 4))
testSuite = TestList [test1, test2]
main = runTestTT testSuite
2. QuickCheck
QuickCheck is a property-based testing library for Haskell, which automatically generates test cases based on properties you define. This can help you discover edge cases or unexpected behaviors you might have missed with manual tests.
To use QuickCheck, add it as a dependency:
dependencies:
- QuickCheck
Import the ‘Test.QuickCheck‘ module and use the ‘quickCheck‘ function along with properties written as functions that return a ‘Bool‘.
Here’s an example of testing the commutative property of the ‘add‘ function:
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Test.QuickCheck
add :: Int -> Int -> Int
add x y = x + y
prop_addCommutative :: Int -> Int -> Bool
prop_addCommutative x y = add x y == add y x
main :: IO ()
main = quickCheck prop_addCommutative
3. Tasty
Tasty is a testing framework that allows you to combine multiple test types like HUnit and QuickCheck tests into a single suite, customize test runners, and organize tests hierarchically.
To use Tasty, add it and other required libraries, such as ‘tasty-hunit‘ and ‘tasty-quickcheck‘, as dependencies:
dependencies:
- tasty
- tasty-hunit
- tasty-quickcheck
Import the necessary modules and use the ‘testGroup‘ function to organize tests into groups, and ‘defaultMain‘ to run all tests:
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Control.Monad
add :: Int -> Int -> Int
add x y = x + y
-- HUnit test
test_add :: TestTree
test_add = testCase "Test add" $ do
4 @?= add 2 2
7 @?= add 3 4
-- QuickCheck property
prop_addCommutative :: Int -> Int -> Bool
prop_addCommutative x y = add x y == add y x
-- Test suite
tests :: TestTree
tests = testGroup "All tests"
[ testGroup "HUnit tests"
[ test_add ]
, testGroup "QuickCheck properties"
[ testProperty "Commutative property of addition" prop_addCommutative ]
]
main :: IO ()
main = defaultMain tests
Tasty provides many customization options, including filtering tests, changing test output format, and using parallelism to speed up test runs.
To summarize, HUnit is suitable for writing simple unit tests, QuickCheck helps generate random test cases based on your defined properties, and Tasty acts as a unifying test framework to organize and run tests from other libraries.