Basic unit testing in Haskell using HUnit and Cabal

asf89

André Ferreira

Posted on December 10, 2022

Basic unit testing in Haskell using HUnit and Cabal

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.

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
Enter fullscreen mode Exit fullscreen mode

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

cabal init 
Enter fullscreen mode Exit fullscreen mode

the initial setting will be like this:

app/
  +- Main.hs
basic-sum.cabal
CHANGELOG.md
Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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

module BasicSum where
Enter fullscreen mode Exit fullscreen mode

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

cabal test
Enter fullscreen mode Exit fullscreen mode

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))
  |                                                    ^^^^^^^^

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
asf89
André Ferreira

Posted on December 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related