Basic unit testing in Haskell using HUnit and Cabal
Tags: haskell, tdd
Published: 2022-12-10 22:30:29

Testing is an essential step in software development and if done early, it can avoid hours of stress in debugging the code. When I came to know Test-Driven Development (TDD), I became an adopter of this way of development and looked to apply it to my code. Another piece of knowledge that I became a fan of was the functional paradigm of programming, which has a more mathematical approach to code thinking.

Therefore, I invite you, reader, to a hike with me so I can show you how to apply TDD with functional programming by making a simple unit test using Haskell, with the HUnit library and  Cabal (Common Architecture for Building Applications and Libraries).

## Motivation

I wanted to apply TDD with Haskell. Although there were numerous sources where I could look, these sources addressed specific points in different styles of making unit tests with Haskell. With this post, I want to establish a basic starting point for those that wish to implement TDD in Haskell without much configuration.

## Prerequisites

For this hike, we need to prepare ourselves. It is necessary to have GHC and Cabal installed on your computer. You can find the installer in the Haskell.org [download section] (Downloads (haskell.org)).

All ready! Let’s go!

## The  Cabal village 

Let’s start our hike by creating a simple folder, named `basic-sum` and get inside it:

```
mkdir basic-sum && cd basic-sum
```

let’s use Cabal to create all the files for our test purposes:

```
cabal init 
```

the initial setting will be like this:

```
app/
  +- Main.hs
basic-sum.cabal
CHANGELOG.md
```

our file of interest will be the `basic-sum.cabal`, which is where we will configure the test suite for our application. Its initial look will be this:

```
cabal-version:      2.4
name:               basic-sum
version:            0.1.0.0
 
-- A short (one-line) description of the package.
-- synopsis:
 
-- A longer description of the package.
-- description:
 
-- A URL where users can report bugs.
-- bug-reports:
 
-- The license under which the package is released.
-- license:
 
-- The package author(s).
-- author:
 
-- An email address to which users can send suggestions, bug reports, and patches.
-- maintainer:
 
-- A copyright notice.
-- copyright:
-- category:
extra-source-files: CHANGELOG.md
 
executable basic-sum
    main-is:          Main.hs
 
    -- Modules included in this executable, other than Main.
    -- other-modules:
 
    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:
    build-depends:    base ^>=4.14.3.0
    hs-source-dirs:   app
    default-language: Haskell2010
 

```

So far, so good!

## Time to camp

Let’s start our camp by mounting our test suite. First, create a directory called `lib`, which will be our library that will contain the functions that we want to test. Inside it, create a file named `BasicSum.hs`. The layout will be like this:

```
app/
  +- Main.hs
lib/
  +- BasicSum.hs
basic-sum.cabal
CHANGELOG.md
```

Now, we need to inform cabal of the existence of this library. In the `basic-sum.cabal` put the following section:
```
cabal-version:      2.4
name:               basic-sum
version:            0.1.0.0
 
-- A short (one-line) description of the package.
-- synopsis:
 
-- A longer description of the package.
-- description:
 
-- A URL where users can report bugs.
-- bug-reports:
 
-- The license under which the package is released.
-- license:
 
-- The package author(s).
-- author:
 
-- An email address to which users can send suggestions, bug reports, and patches.
-- maintainer:
 
-- A copyright notice.
-- copyright:
-- category:
extra-source-files: CHANGELOG.md
 
library basic-sum-lib
    exposed-modules: BasicSum
    hs-source-dirs: lib
    build-depends: base ^>=4.14
    default-language: Haskell2010
 
executable basic-sum
    main-is:          Main.hs
 
    -- Modules included in this executable, other than Main.
    -- other-modules:
 
    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:
    build-depends:    base ^>=4.14.3.0
    hs-source-dirs:   app
    default-language: Haskell2010
 
```
Now to create our test suite. Create a directory called `tests` and inside it create a file named `BasicSumTest.hs`. The layout should be like the following:

```
app/
  +- Main.hs
lib/
  +- BasicSum.hs
tests/
  +- BasicSumTest.hs
basic-sum.cabal
CHANGELOG.md
```

Now we add the following section to our `basic-sum.cabal` file:
```
cabal-version:      2.4
name:               basic-sum
version:            0.1.0.0
 
-- A short (one-line) description of the package.
-- synopsis:
 
-- A longer description of the package.
-- description:
 
-- A URL where users can report bugs.
-- bug-reports:
 
-- The license under which the package is released.
-- license:
 
-- The package author(s).
-- author:
 
-- An email address to which users can send suggestions, bug reports, and patches.
-- maintainer:
 
-- A copyright notice.
-- copyright:
-- category:
extra-source-files: CHANGELOG.md
 
library basic-sum-lib
    exposed-modules: BasicSum
    hs-source-dirs: lib
    build-depends: base ^>=4.14
    default-language: Haskell2010
 
executable basic-sum
    main-is:          Main.hs
 
    -- Modules included in this executable, other than Main.
    -- other-modules:
 
    -- LANGUAGE extensions used by modules in this package.
    -- other-extensions:
    build-depends:    base ^>=4.14.3.0
    hs-source-dirs:   app
    default-language: Haskell2010
 
test-suite tests
    type: exitcode-stdio-1.0
    main-is: BasicSumTest.hs
    build-depends: base ^>=4.14, HUnit ^>=1.6, basic-sum-lib
    hs-source-dirs: tests
    default-language: Haskell2010
 
```

## Following the trail of TDD

Let’s make a test. In the file `BasicSumTest.hs`, we will import the function library `BasicSum` (in the `lib` folder), which has the function `basicSum` that takes two integers and return the sum, the `HUnit` library, and the `System.Exit`, which outputs the success or failure of the test:

```
module Main where
import BasicSum
import Test.HUnit
import qualified System.Exit as Exit
 
test1 :: Test
test1 = TestCase (assertEqual "should return 3" 3 (basicSum 1 2))
 
tests :: Test
tests = TestList [TestLabel "test1" test1]
 
main :: IO ()
main = do
    result <- runTestTT tests
    if failures result > 0 then Exit.exitFailure else Exit.exitSuccess
 
 
```

In the `BasicSum.hs`, just put the following line:

```
module BasicSum where
```

So the `BasicSum` library be recognized by Cabal. Now let’s run the test executing the following command in the terminal:

```
cabal test
```

The result should be this:

```
Build profile: -w ghc-8.10.7 -O1
In order, the following will be built (use -v for more details):
 - basic-sum-0.1.0.0 (lib:basic-sum-lib) (first run)
 - basic-sum-0.1.0.0 (test:tests) (first run)
Preprocessing library 'basic-sum-lib' for basic-sum-0.1.0.0..
Building library 'basic-sum-lib' for basic-sum-0.1.0.0..
[1 of 1] Compiling BasicSum         ( personal_info)
Configuring test suite 'tests' for basic-sum-0.1.0.0..
Preprocessing test suite 'tests' for basic-sum-0.1.0.0..
Building test suite 'tests' for basic-sum-0.1.0.0..
[1 of 1] Compiling Main             (personal_info)

tests/BasicSumTest.hs:7:52: error:
    Variable not in scope: basicSum :: t0 -> t1 -> a0
  |
7 | test1 = TestCase (assertEqual "should return 3" 3 (basicSum 1 2))
  |                                                    ^^^^^^^^

```

It is expected that an error occurs, don’t worry. Following the trail of TDD, you first create a test that fails, then you start to develop the code to make the test pass. Now we create the function `basicSum`  in the `BasicSum` library:

```
module BasicSum where

basicSum :: Int -> Int -> Int
basicSum x y = x + y
```

Now we execute `cabal test` again and the result should be:

```
Build profile: -w ghc-8.10.7 -O1
In order, the following will be built (use -v for more details):
 - basic-sum-0.1.0.0 (lib:basic-sum-lib) (file lib/BasicSum.hs changed)
 - basic-sum-0.1.0.0 (test:tests) (dependency rebuilt)
Preprocessing library 'basic-sum-lib' for basic-sum-0.1.0.0..
Building library 'basic-sum-lib' for basic-sum-0.1.0.0..
[1 of 1] Compiling BasicSum         (personal_info)
Preprocessing test suite 'tests' for basic-sum-0.1.0.0..
Building test suite 'tests' for basic-sum-0.1.0.0..
[1 of 1] Compiling Main             (personal_info)
Linking <path_to_the_test_info_in_cabal>
Running 1 test suites...
Test suite tests: RUNNING...
Test suite tests: PASS
Test suite logged to:
<path_to_the_test_log>
1 of 1 test suites (1 of 1 test cases) passed.
```

## The End of the Hike

That’s it! Thanks for the company! Feedback is appreciated, I am constantly improving so I can help more and more people. Until next time!