Providence Salumu
First published: June 14, 2016
by Juan Carlos Pazmiño
Tested with:
“Do the simplest thing that could possibly work” - Kent Beck
Test Driven Development (TDD) is an iterative approach of software development that promotes a very simple idea: test before you code. It encourages developers to write tests first, and then write the minimum necessary amount of code for the tests to pass, repeating the process again, in small incremental iterations.
Behavior Driven Development (BDD) goes a step further by describing not tests but the behavior a piece of code should show to an outside observer. It provides semantics shifted towards specification rather than testing.
Haskell offers a strong type system that guarantees code correctness. So, why would you ever bother to use BDD in your Haskell projects? Although Haskell does help us with this, when it comes to ensure that the code does what's expected, you're the only one responsible and BDD comes in handy. Common uses of BDD include preventing division by zero cases, or ensuring your own instances of monoids, functors, monads, etc., follow the expected rules.
Hspec is a Haskell library that provides an embedded domain specific language (EDSL) for defining BDD specs. A spec is organized in a tree structure defined in terms of describe
and it
.
The describe
clause shows the name of the function or feature whose behavior we are going to specify. This clause can contain multiple it
clauses that show a textual description of the expected behavior.
And inside the it
clauses, we place the expectations. Expectations are implementations of the expectancy of a certain behavior, and commonly use the word should
. Examples of this expectation functions are shouldBe
, shouldSatisfy
, etc.
While trying to choose the "perfect" sample for a BDD tutorial, I got suddenly inspired by one of the first exercises I saw in the CIS 194 course. Personally, I consider it a great Haskell introduction material.
Luhn is an algorithm used to validate credit card numbers. The algorithm is very simple in its conception. Thus, it's a great option to watch BDD in action.
Given an integer number, the Luhn algorithm follows these steps:
[1,3,8,6]
becomes [2,3,16,6]
.[2,3,16,6]
becomes 2+3+1+6+6 = 18
.The fastest way to start an Hspec-enabled project is by creating a new Stack project using the hspec template:
$ stack new luhn hspec
$ cd luhn
This command will create a new folder with the necessary files and configuration to create Hspec specifications. The important bits are:
luhn.cabal
file with a test-suite configuration which includes, amongst others, hspec
and QuickCheck
as build dependencies. QuickCheck
is a library that allows creating property tests in Haskell, but more on that later:test-suite luhn-test
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Spec.hs
build-depends: base
, luhn
, hspec
, QuickCheck
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010
src
folder with a sample module Data.String.Strip
.test
folder with an Spec.hs
test driver file, containing configuration options that allow Hspec to find your specs without the need to include them manually. You shouldn't change this file:{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
The project has all it needs to run the specs, out of the box. So, why don't you try the following command and see what happens:
$ stack build --test
Hspec's output will give you some useful information like the number of specs, the described features and what is expected from them in color coded text (green for passing specs and red for failing ones), and a small summary.
You can take a look at the project structure and sample code if you want. For the purpose of this tutorial there are some files we don't need anymore, so we'll start by removing the src/Data/
and test/Data
folders completely.
This tutorial relies only on Hspec tests. We are not going to create an executable application so we can also remove the app
folder.
At the luhn.cabal
file:
executable luhn-exe
section entirely.Data.String.Strip
module and add the Luhn
and Luhn.Internal
modules (we'll create these modules next). The library
section should end up looking like this:library
hs-source-dirs: src
exposed-modules: Luhn
, Luhn.Internal
build-depends: base >= 4.7 && < 5
default-language: Haskell2010
Lastly we'll add some shell content for the project to compile:
$ echo "module Luhn where" > src/Luhn.hs
$ mkdir src/Luhn
$ echo "module Luhn.Internal where" > src/Luhn/Internal.hs
Run the specs again and everything should be green. We should have 0 examples now.
When programming the BDD way, you start writing the specs first. This is not only a good practice, it also helps to reason about your code as a series of individual decoupled units.
For the Luhn algorithm, we'll need a validate :: Integer -> Bool
function that receives a number and determines if it is valid or not. Based on the Luhn algorithm description, we can foresee the need of the following helper functions:
toDigits :: Integer -> [Integer]
to convert an integer number to a list of digits.doubleEveryOther :: [Integer] -> [Integer]
to double every other digit starting from the right.sumDigits :: [Integer] -> Integer
to sum all digits, ensuring doubled digits sum its own digits first.As good practice, some developers split their code into two source files. One of the modules will contain the function we want exposed publicly to the whole project. The other one (commonly suffixed as .Internal
) will hold the helper functions.
You should avoid importing the Internal modules in your application, but having this file around is useful whenever you want to import them in order to test its functions.
So we are going to edit our src/Luhn.hs
module file and create a stub function. The file should look like this:
module Luhn
( validate
)
where
import Luhn.Internal
validate :: Integer -> Bool
validate = undefined
Then, we edit our src/Luhn/Internal.hs
module file and insert the following code:
module Luhn.Internal
( toDigits
, doubleEveryOther
, sumDigits
)
where
toDigits :: Integer -> [Integer]
toDigits = undefined
doubleEveryOther :: [Integer] -> [Integer]
doubleEveryOther = undefined
sumDigits :: [Integer] -> Integer
sumDigits = undefined
Even if we're supposed to start with the spec, having these undefined definitions at hand will help us compile the spec and run them.
Let's create the test/LuhnSpec.hs
file inside the test
folder. The name of the folder is important here. In order for Hspec to find our spec files you should follow certain rules:
Spec.hs
file, or into a subdirectory.Spec.hs
; the module name has to match the file name.Spec
.Now, let's add a little bit of boilerplate code, starting with our imports:
module LuhnSpec (main, spec) where
import Test.Hspec
import Test.QuickCheck
import Luhn
import Luhn.Internal
We import our spec targets Luhn
and Luhn.Internal
.
Now, we create our main
function that simply calls hspec
sending a Spec
value as parameter:
main :: IO ()
main = hspec spec
Finally, we create our Spec
object with the first spec. We need to ensure toDigits
behaves as expected:
spec :: Spec
spec = do
describe "toDigits" $ do
it "converts a number to a list of digits" $ do
toDigits 1234567 `shouldBe` [1,2,3,4,5,6,7]
We'll destructure this code a bit:
spec
contains one or many describe
clauses.describe
clause receives a String
with the name of the function or feature we are going to specify. This clause can contain one or more it
clauses.it
clause receives a String
describing the expected behavior for the function or feature. This clause can contain one or more expectations.shouldBe
expectation here, which simply expects its two operands to be equal. We'll use a couple more later.The current structure lets us read our spec as "toDigits, converts a number to a list of digits". This is, at least, the behavior we expect to see.
If we run the spec now, it should fail with an uncaught exception because toDigits
is not yet implemented.
Let's add the minimal implementation needed for the spec to pass. We'll do this in the Luhn.Internal
module:
toDigits :: Integer -> [Integer]
toDigits _ = [1,2,3,4,5,6,7]
Run the spec again and voilà! The spec passed. Now, let's add another expectation to be sure. Add this code just below our previous expectation and run the spec again:
toDigits 2468 `shouldBe` [2,4,6,8]
It fails! Easy enough, we could modify the function to return the reversed list this time. That will make the first expectation fail. As naive as this game seems, it illustrates what happens in real life, once you have written larger amounts of code.
You can have the task to add new features to your application. So, you start refactoring your code to make your new feature work but then you unknowingly break some other feature you wrote days or even months ago. Without specs, you'll be lucky if you catch this error before they hit production. You would have been warned about this in a very early stage, with the right specs.
Let's implement this function, in a way that makes more sense, to make both expectations pass:
toDigits :: Integer -> [Integer]
toDigits n
| n <= 0 = []
| otherwise = m : toDigits d where (d,m) = divMod n 10
Feeling proud about our achievements, we save the file, hit the console, run 'em specs and ... still didn't pass. Not to worry. Hspec's feedback can help us out. Just look at the output Hspec displays. You should see something like this:
test/LuhnSpec.hs:16:
1) Luhn.toDigits "converts a number to a list of digits"
expected: [1,2,3,4,5,6,7]
but got: [7,6,5,4,3,2,1]
We are getting the inverse of the expected result. So we change our code respectively:
toDigits :: Integer -> [Integer]
toDigits = go []
where go xs n
| n <= 0 = xs
| otherwise = go (m:xs) d where (d,m) = divMod n 10
Running the specs again everything should go green, and we regain our confidence. Let's move on to the doubleEveryOther
function. Think about it. What do we expect?
describe "doubleEveryOther" $ do
it "doubles every other number starting from the right" $ do
doubleEveryOther [1,2,3,4,5] `shouldBe` [1,4,3,8,5]
The specs shouldn't pass due to an uncaught exception again.
This time it's your turn. Give it a try and create an implementation for this function. Remember not to start with a fancy solution right away. Just write what it's needed for the spec to pass.
Now, we'll add another expectation to reassure our code behaves:
doubleEveryOther [1,2,3,4,5,6] `shouldBe` [2,2,6,4,10,6]
Specs fail again, of course. As a hint, having even and odd number of elements in the list might complicate our algorithm. The use of functions like reverse
might help. Improve your code in small steps until the specs are green one more time.
You can always take a look at the sample project accompanying this tutorial and compare your results. Code doesn't need to match. As long as you get the right results and specs pass, it's alright. And, remember to run your specs after each step to gain immediate feedback.
Sometimes you need to write specs for a single function or feature, when used in just a couple of different environments. You could use it
clauses describing the input properties every time. Or, you could use a context
. A context
groups one or many it
clauses that share the same kind of input or environment.
In particular, we want to test sumDigits
behavior when all of the digit numbers are less than ten. Also, test the same function when some of the numbers are greater or equal to ten.
We start by creating the context and expectations for the first case:
describe "sumDigits" $ do
context "when all numbers are less than 10" $ do
it "sums the list of integers" $ do
sumDigits [1,2,3,4,5,6] `shouldBe` sum [1,2,3,4,5,6]
Why don't you give it a try and implement sumDigits
? Again, you can look at the source code to compare your results.
After you implement sumDigits
, let's add another context to describe the behavior of the function if the list contains some numbers greater or equal to ten. This numbers should first sum their digits prior to summing the list itself:
context "when some numbers are greater or equal to 10" $ do
it "sums their digits first before summing the list" $ do
-- 2+1+2+4+1+4+6+8 = 28
sumDigits [2,12,4,14,6,8] `shouldBe` 28
Can you modify the sumDigits
implementation to match the new requirements?
QuickCheck is a Haskell library that allows testing properties rather than expectations. This means that if you have some piece of code which must hold on a property that should always be true, you can use QuickCheck to feed that property, with as much arbitrary inputs as possible, to check if it really holds.
For educative purposes we'll add a fromDigits
function. This function should do the opposite of what toDigits
does. It should take a list of digits and convert it to a number.
Let's begin with some code as if we don't have QuickCheck around. We can start by adding this code to the describe "toDigits"
section in the LuhnSpec
file:
it "holds on: x == fromDigits(toDigits x)" $ do
12345 `shouldBe` fromDigits (toDigits 123456)
And then add this simple implementation to the Luhn.Internal
module. Remember to expose this function in the module
clause:
fromDigits :: [Integer] -> Integer
fromDigits _ = 12345
Spec passes and everyone is happy. Now, let's replace the spec we just wrote with a QuickCheck property instead:
it "holds on the property x == fromDigits(toDigits x)" $ do
property $ \x -> x == (fromDigits . toDigits) x
Here we are creating a property. The property receives a function that will receive arbitrary values of the inferred type and must return a Bool
. In this case, x
is going to be of type Integer
and we use it to assert the property.
Under the hood QuickCheck will feed the property function with many arbitrary values, and the property checks if, and only if, for all cases, the function returns True
.
After running the specs and checking that the property doesn't hold, we add a suitable implementation:
fromDigits :: [Integer] -> Integer
fromDigits = sum . zipWith (*) [10 ^ i | i <- [0..]] . reverse
It didn't pass. What's going on? Let's check the Hspec display:
test/LuhnSpec.hs:22:
1) Luhn.toDigits holds on: x == fromDigits(toDigits x)
Falsifiable (after 3 tests):
-1
Aha! The property doesn't hold for negative numbers. That's alright, after all, this was the expected behavior. The toDigits
function returns an empty list for numbers lower or equal to zero. So we cannot recover the same number back with the fromDigits
function.
The solution is to prevent QuickCheck from feeding negative numbers. QuickCheck's (==>)
operator offers a way to specify arbitrary prerequisites for your properties.
We just place the conditions you want the arbitrary values to comply, to the left of the operator and your property check to the right. With this in mind, we can rewrite our spec once more:
property $ \x ->
x >= 0 ==> x == (fromDigits . toDigits) x
We are forcing the x
values to be greater or equal to zero. This time around, it works like a charm.
So, we've implemented all helper functions. However, the main validate
function is still missing. The spec should look like this:
describe "validate" $ do
it "returns True if number is valid, False otherwise" $ do
1234567889 `shouldSatisfy` validate
1234567887 `shouldNotSatisfy` validate
Wait a minute. We are using different expectation functions, shouldSatisfy
and shouldNotSatisfy
to be more precise. These functions receive a predicate function of type Show a => a -> Bool
as its second parameter. So basically, this predicate must receive a parameter of any showable type and return a Bool
value. And given our validate
function receives an Integer
and returns a Bool
, it fits perfectly.
After some iterations we might come up with an implementation for validate
similar to this one, in our Luhn
module ...
validate :: Integer -> Bool
validate = (== 0) . (`mod` 10) . sumDigits . doubleEveryOther . toDigits
... which is simply a composition of all of our previous helper functions. Just run the specs one final time and, if everything is green, let's consider it done.
In this tutorial we reviewed some concepts like TDD and BDD. We also gave you a small taste of, what it is, and what it feels like writing specs the TDD/BDD way. Additionally, we used a bunch of expectation functions and QuickCheck properties.
There is more to BDD than what we've learned in this tutorial. Here are some other follow-up resources you can check:
Happy testing! or should I say ... specification?
Thanks for reading this tutorial! If you have any feedback, please join the discussion below, or open issues and pull requests on GitHub.