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
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!
Top comments (0)