HUnit 1.0 User's Guide
From HaskellWiki
The HUnit software and this guide were written by Dean Herington (heringto@cs.unc.edu).
HUnit is a unit testing framework for Haskell, inspired by the JUnit tool for Java. This guide describes how to use HUnit, assuming you are familiar with Haskell, though not necessarily with JUnit.
Contents |
1 Introduction
A test-centered methodology for software development is most effective when tests are easy to create, change, and execute. The JUnit tool pioneered support for test-first development in Java. HUnit is an adaptation of JUnit to Haskell, a general-purpose, purely functional programming language. (To learn more about Haskell, see http://www.haskell.org.)
With HUnit, as with JUnit, you can easily create tests, name them, group them into suites, and execute them, with the framework checking the results automatically. Test specification in HUnit is even more concise and flexible than in JUnit, thanks to the nature of the Haskell language. HUnit currently includes only a text-based test controller, but the framework is designed for easy extension. (Would anyone care to write a graphical test controller for HUnit?)
The next section helps you get started using HUnit in simple ways. Subsequent sections give details on writing tests and running tests. The document concludes with a section describing HUnit's constituent files and a section giving references to further information.
2 Getting Started
In the Haskell module where your tests will reside, import moduleimport Test.HUnit
Define test cases as appropriate:
test1 = TestCase (assertEqual "for (foo 3)," (1,2) (foo 3)) test2 = TestCase (do (x,y) <- partA 3 assertEqual "for the first result of partA," 5 x b <- partB y assertBool ("(partB " ++ show y ++ ") failed") b)
Name the test cases and group them together:
tests = TestList [TestLabel "test1" test1, TestLabel "test2" test2]
> runTestTT tests Cases: 2 Tried: 2 Errors: 0 Failures: 0 >
If the tests are proving their worth, you might see:
> runTestTT tests ### Failure in: 0:test1 for (foo 3), expected: (1,2) but got: (1,3) Cases: 2 Tried: 2 Errors: 0 Failures: 1 >
Isn't that easy?
You can specify tests even more succinctly using operators and overloaded functions that HUnit provides:
tests = test [ "test1" ~: "(foo 3)" ~: (1,2) ~=? (foo 3), "test2" ~: do (x, y) <- partA 3 assertEqual "for the first result of partA," 5 x partB y @? "(partB " ++ show y ++ ") failed" ]
Assuming the same test failures as before, you would see:
> runTestTT tests ### Failure in: 0:test1:(foo 3) expected: (1,2) but got: (1,3) Cases: 2 Tried: 2 Errors: 0 Failures: 1 >
3 Writing Tests
Tests are specified compositionally. Assertions are combined to make a test case, and test cases are combined into tests. HUnit also provides advanced features for more convenient test specification.
3.1 Assertions
The basic building block of a test is an assertion.
type Assertion = IO ()
assertFailure :: String -> Assertion assertFailure msg = ioError (userError ("HUnit:" ++ msg))
assertBool :: String -> Bool -> Assertion assertBool msg b = unless b (assertFailure msg) assertString :: String -> Assertion assertString s = unless (null s) (assertFailure s) 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
3.2 Test Case
A test case is the unit of test execution. That is, distinct test cases are executed independently. The failure of one is independent of the failure of any other.
A test case consists of a single, possibly collective, assertion. The possibly multiple constituent assertions in a test case's collective assertion are not independent. Their interdependence may be crucial to specifying correct operation for a test. A test case may involve a series of steps, each concluding in an assertion, where each step must succeed in order for the test case to continue. As another example, a test may require some "set up" to be performed that must be undone ("torn down" in JUnit parlance) once the test is complete. In this case, you could use Haskell's3.3 Tests
As soon as you have more than one test, you'll want to name them to tell them apart. As soon as you have more than several tests, you'll want to group them to process them more easily. So, naming and grouping are the two keys to managing collections of tests.
In tune with the "composite" design pattern 1], a test is defined as a package of test cases. Concretely, a test is either a single test case, a group of tests, or either of the first two identified by a label.
data Test = TestCase Assertion | TestList [Test] | TestLabel String Test
There are three important features of this definition to note:
- A consists of a list of tests rather than a list of test cases. This means that the structure of aTestListis actually a tree. Using a hierarchy helps organize tests just as it helps organize files in a file system.Test
- A is attached to a test rather than to a test case. This means that all nodes in the test tree, not just test case (leaf) nodes, can be labeled. Hierarchical naming helps organize tests just as it helps organize files in a file system.TestLabel
- A is separate from bothTestLabelandTestCase. This means that labeling is optional everywhere in the tree. Why is this a good thing? Because of the hierarchical structure of a test, each constituent test case is uniquely identified by its path in the tree, ignoring all labels. Sometimes a test case's path (or perhaps its subpath below a certain node) is a perfectly adequate "name" for the test case (perhaps relative to a certain node). In this case, creating a label for the test case is both unnecessary and inconvenient.TestList
testCaseCount :: Test -> Int
As mentioned above, a test is identified by its path in the test hierarchy.
data Node = ListItem Int | Label String deriving (Eq, Show, Read) type Path = [Node] -- Node order is from test case to root.
Note that the order of nodes in a path is reversed from what you might expect: The first node in the list is the one deepest in the tree. This order is a concession to efficiency: It allows common path prefixes to be shared.
The paths of the test cases that a test comprises can be computed withtestCasePaths :: Test -> [Path]
- Combining assertions and other code to construct test cases is easy with the monad.IO
- Using overloaded functions and special operators (see below), specification of assertions and tests is extremely compact.
- Structuring a test tree by value, rather than by name as in JUnit, provides for more convenient, flexible, and robust test suite specification. In particular, a test suite can more easily be computed "on the fly" than in other test frameworks.
- Haskell's powerful abstraction facilities provide unmatched support for test refactoring.
3.4 Advanced Features
HUnit provides additional features for specifying assertions and tests more conveniently and concisely. These facilities make use of Haskell type classes.
The following operators can be used to construct assertions.
infix 1 @?, @=?, @?= (@?) :: (AssertionPredicable t) => t -> String -> Assertion pred @? msg = assertionPredicate pred >>= assertBool msg (@=?) :: (Eq a, Show a) => a -> a -> Assertion expected @=? actual = assertEqual "" expected actual (@?=) :: (Eq a, Show a) => a -> a -> Assertion actual @?= expected = assertEqual "" expected actual
type AssertionPredicate = IO Bool class AssertionPredicable t where assertionPredicate :: t -> AssertionPredicate instance AssertionPredicable Bool where assertionPredicate = return instance (AssertionPredicable t) => AssertionPredicable (IO t) where assertionPredicate = (>>= assertionPredicate)
class Assertable t where assert :: t -> Assertion instance Assertable () where assert = return instance Assertable Bool where assert = assertBool "" instance (ListAssertable t) => Assertable [t] where assert = listAssert instance (Assertable t) => Assertable (IO t) where assert = (>>= assert)
class ListAssertable t where listAssert :: [t] -> Assertion instance ListAssertable Char where listAssert = assertString
class Testable t where test :: t -> Test instance Testable Test where test = id instance (Assertable t) => Testable (IO t) where test = TestCase . assert instance (Testable t) => Testable [t] where test = TestList . map test
The following operators can be used to construct tests.
infix 1 ~?, ~=?, ~?= infixr 0 ~: (~?) :: (AssertionPredicable t) => t -> String -> Test pred ~? msg = TestCase (pred @? msg) (~=?) :: (Eq a, Show a) => a -> a -> Test expected ~=? actual = TestCase (expected @=? actual) (~?=) :: (Eq a, Show a) => a -> a -> Test actual ~?= expected = TestCase (actual @?= expected) (~:) :: (Testable t) => String -> t -> Test label ~: t = TestLabel label (test t)
4 Running Tests
HUnit is structured to support multiple test controllers. The first subsection below describes the test execution characteristics common to all test controllers. The second subsection describes the text-based controller that is included with HUnit.
4.1 Test Execution
All test controllers share a common test execution model. They differ only in how the results of test execution are shown.
The execution of a test (a value of typedata Counts = Counts { cases, tried, errors, failures :: Int } deriving (Eq, Show, Read)
- is the number of test cases included in the test. This number is a static property of a test and remains unchanged during test execution.cases
- is the number of test cases that have been executed so far during the test execution.tried
- is the number of test cases whose execution ended with an unexpected exception being raised. Errors indicate problems with test cases, as opposed to the code under test.errors
- is the number of test cases whose execution asserted failure. Failures indicate problems with the code under test.failures
As test execution proceeds, three kinds of reporting event are communicated to the test controller. (What the controller does in response to the reporting events depends on the controller.)
- start
- Just prior to initiation of a test case, the path of the test case and the current counts (excluding the current test case) are reported.
- error
- When a test case terminates with an error, the error message is reported, along with the test case path and current counts (including the current test case).
- failure
- When a test case terminates with a failure, the failure message is reported, along with the test case path and current counts (including the test case).
Typically, a test controller shows error and failure reports immediately but uses the start report merely to update an indication of overall test execution progress.
4.2 Text-Based Controller
A text-based test controller is included with HUnit.
runTestText :: PutText st -> Test -> IO (Counts, st)
The strings for the three kinds of reporting event are as follows.
- A start report is the result of the function applied to the counts current immediately prior to initiation of the test case being started.showCounts
- An error' report is of the form "", where path is the path of the test case in error, as shown byError in: ''path''\n''message'', and message is a message describing the error. If the path is empty, the report has the form "showPath".Error:\n''message''
- A failure report is of the form "", where path is the path of the test case in error, as shown byFailure in: ''path''\n''message'', and message is the failure message. If the path is empty, the report has the form "showPath".Failure:\n''message''
showCounts :: Counts -> String
showPath :: Path -> String
HUnit includes two reporting schemes for the text-based test controller. You may define others if you wish.
putTextToHandle :: Handle -> Bool -> PutText Int
putTextToShowS :: PutText ShowS
HUnit provides a shorthand for the most common use of the text-based test controller.
runTestTT :: Test -> IO Counts
5 References
- [1] Gamma, E., et al. Design Patterns
- Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995.
- The classic book describing design patterns in an object-oriented context.
- http://www.junit.org
- Web page for JUnit, the tool after which HUnit is modeled.
- http://junit.sourceforge.net/doc/testinfected/testing.htm
- A good introduction to test-first development and the use of JUnit.
- http://junit.sourceforge.net/doc/cookstour/cookstour.htm A description of the internal structure of JUnit. Makes for an interesting comparison between JUnit and HUnit.
