Outside-in TDD with F#
I wanted to experience outside-in TDD with F# so I decided to revisit the Bank Kata which I explored earlier this month in C#. How different would the result be?
First things first. I am by no means an F# expert. On the contrary. I explored it last year and I used it to prototype some business code. That’s it! I haven’t used it in earnest yet. So if you are well-versed in F# and you discover that I could have taken a more idiomatic approach please let me know in the comments. I would love to learn from you.
Just like last time I tried to remember committing often so you can see the full evolution in this repo on Github:
F# version of the Bank Kata. Contribute to eriksacre/BankKataFs development by creating an account on GitHub.github.com
Libraries & tools
This is my setup:
- Rider 2018.2
- .Net Core 2.1
Rider delivered a great experience when I did the C# version of the Kata. However the support for F# has some quirks and bugs. Especially after renaming a file or after switching a branch I often needed to restart the IDE.
I like the syntax FsUnit provides but I’m a bit disappointed with the output when the assertion fails. Especially when comparing lists the output can be useless and leave you without any clue as to what is wrong.
Introducing the Bank Kata
The Bank Kata is rather simple. The purpose is to create 3 commands that operate on an Account:
- Deposit: put some amount into the account
- Withdraw: take some amount out of the account
- PrintStatement: print a full statement to the console
In the F# version of the Kata every command is implemented in a function. These three functions are the only public interface to an ‘AccountService’ module.
A printed statement looks like this:
DATE | AMOUNT | BALANCE
10/04/2014 | 500.00 | 1400.00
02/04/2014 | -100.00 | 900.00
01/04/2014 | 1000.00 | 1000.00
To keep things simple there is no need to mess with formatting, so additional spaces to align columns are not required.
Start from the outside: feature test
Since this is an exercise in outside-in test-driven development we will start from the outside by writing a feature test, a.k.a. an acceptance test. The feature test should result in the printed statement shown above:
Most of this test is obvious. We simply call Deposit, Withdraw and PrintStatement to exercise the code. But the production code writes its output to the console. So in this test we need some way to capture that output.
For now we’ll assume there is a function ‘capturedOutput’ that returns a list of the messages written to the console.
We add just enough code to make this compile. Of course the test still fails because we haven’t implemented anything.
Let’s dive in: TDD-ing the Deposit function
So now we start moving from the outside to the inside. The first function on our path is the ‘Deposit’ function. Its responsibility is putting a transaction in the repository with the given amount and today’s date.
We are really testing how this function makes use of other functions rather than testing a specific implementation. We need mocking and stubbing… but we will not be using some fancy mocking framework in this Kata!
A stub is as simple as the ‘today’ binding you see in the code above. It simply is a function that returns some hard-coded value. This is fine to get today’s date.
Another challenge is knowing the transaction was added to the repository without having a real repository that can be queried. Here we need some kind of mock or spy. I’ve created a simple helper module called ‘Fake’ (not to be confused with the F# make library). The ‘Set’ function returns two functions: a setter and a getter. In the code above they are bound to the names ‘add’ and ‘added’. Writing this paragraph made me realize how poorly chosen the names Fake & Set are… Naming: one of the hardest problems in computer science!
The key to making it all work is partial application. We declare ‘deposit’ to be the ‘Account.Deposit’-function with the ‘today’ and ‘add’ functions already applied. The resulting signature of ‘deposit’ is ‘Amount -> unit’ which is exactly what our public ‘Deposit’ function should be.
type Deposit = Amount -> unit
Once this setup is done the actual testing becomes trivial. Calling the deposit function with some amount and verifying the add function was called with a transaction containing today’s date and the given amount.
The sensible next step would be to implement the ‘Add’ function of the repository. But somehow the code did not tell me this needed to be done, so the next bump on the road to making the feature test pass was the ‘Withdraw function’.
It’s nearly identical to the ‘Deposit’-function, both the test and the implementation.
I see some duplication
Although this Kata has very little code one can spot some duplication in the following:
It probably is nitpicking but calling the add function and passing a nearly identical tuple twice is something I’d rather not see.
So I thought I would add a helper function to extract this common behaviour. Out came ‘StoreTransaction’:
Now ‘Deposit’ and ‘Withdraw’ are reduced to:
And this is where I was not very consistent. The ‘Deposit/Withdraw’ functions get ‘StoreTransaction’ as a parameter.
let storeTransaction = Transaction.StoreTransaction today add
let deposit = Account.Deposit storeTransaction
Shouldn’t the test for ‘Deposit/Withdraw’ become about verifying ‘StoreTransaction’ was called instead of ‘add’ and ‘today’?
This is where my classicist way of working took charge. I considered ‘StoreTransaction’ an implementation detail. It is not tested in isolation.
I would probably do one of two things here:
- Either go all the way and consider StoreTransaction in isolation, just like we will be considering ‘StatementPrinter’ in isolation
- Or simply see ‘StoreTransaction’ as a helper function, but in that case it should not be a parameter to ‘Deposit/Withdraw’
Let’s print some statements
We’re making good progress. The next step in making our feature test pass is implementing ‘PrintStatement’. Now printing a statement seems like a big responsibility which I’d rather delegate. So the main module’s ‘PrintStatement’ function will have limited reponsibility:
- Call the Print function in the StatementPrinter module…
- …passing in the list of all transactions
This function is all about testing interaction:
The implementation can be summarized with this one-liner:
getTransactions () |> print
Oh my… complexity!
Let’s tackle the Print function. This is the most complex piece of this Kata. We must print a list of statements to the console. A statement contains the date and the amount as well as a running balance. The list must be anti-chronological.
Seems like a meaty challenge:
Key to this test is the ability to capture the console output. We briefly touched upon it when describing the feature test, but now we need it for real.
The ‘Console.Capture’-function returns two functions:
- printLine, which can be used to… print a line to a fake console
- printed, which is used to fetch a list of all printed messages
The ‘printLine’ function is passed to the ‘Print’ function.
I explicitly avoided having to pass the ‘printLine’ function any deeper into the code. I wanted the core logic to consist of pure functions. So ‘Print’ calls ‘header’ and ‘statements’. ‘statements’ is the interesting one. It turns the transactions into a formatted list of strings that include the running balance.
Some persistence required
At this point the bulk of the work is completed. There is one key piece missing to make the feature test pass: a repository.
I’ve implemented a very simple in-memory repository. It has two functions:
- add: to add a transaction to the repository
- getTransactions: to get a list of all transactions in insertion order
The tests and the implementation are trivial:
Feature test turns green
We need a decent chunk of setup code:
The only new bit is another helper function in my Fake-module: ‘ReturnValues’. It returns a function that will return the given values one by one. In this case we are simulating the passing of days. For every transaction that we’ll add via ‘Deposit’ or ‘Withdraw’ the next date in the given list will be used.
And with this code our feature test passes as well as all the other tests.
Overall it seems the approach to outside-in TDD is very similar between my two attempts. The high-level design is identical, although the details are very different if only because of the differences between object oriented and functional programming.
F# charmed me last year, and this sensation came back while doing this Kata. I really like the language. It is nice, clear & concise. I’m sure I’m not doing things the idiomatic way. As I wrote in the introduction I value any feedback you may have.
One thing that struck me is how easy it was to do the equivalent of mocking and stubbing.
I really enjoyed doing this Kata in F#. Just for comparison: here is the link to my article about the C# version: