Ever since I switched from C# to F#, I’ve been amazed at how many of the language features allow developers to optimise building the thing right (immutability, pattern matching & static types to name but a few), however to ensure I’m building the right thing I always fall back on behaviour driven development (BDD).
With BDD we use behaviours to describe the function of a system using a common domain language. This facilitates communication with our stakeholders, which in turn makes it much easier for all involved to ensure they have a common understanding (gone are the “but that’s not what I meant” or “I thought you meant…” moments)
Here is a basic discount calculator in Gherkin, which has a Feature that contains a single Scenario Outline:
Within the scenario there are 3 types of step;
- Given sets-up the context of the behaviour (create a list of registered users)
- When is the action we are specifying (spending some money)
- Then asserts that the result of the action meets our expectations (the discount has or hasn’t been applied to the total)
A Scenario Outline allows the same scenario to be run multiple times with different values from the Examples tables.
Data from the Examples is substituted in the Scenario Outline when the example is run — the symbols < and > enclose a parameter.
In this case, the Scenario Outline will be executed 4 times:
- Mary although registered & eligible, doesn’t meet the spend requirement, so no discount
- John is registered & eligible and has met the spend requirements, so the 10% discount is applied to his total
- Richard is registered and has spent enough, but is not eligible for a discount so pays the full amount
- Sarah isn’t even registered so pays the full amount
So what does this have to do with F#?
The reason I like working with behaviours is because they allow me to describe what a system should do without specifying how it does it -behaviours are lightweight and compose-able — the registered customer’s eligibility for the discount is a different set of behaviours involving factors such as the customers current balance, friend referral rate, previous spending history, each of which are composed of other behaviours.
This sounds an awful lot like the F# type system to me — we should be able to map from one to the other.
Lets have a go…..
Starting at the top of our feature there is the line “Feature: Discount Calculation” followed by a Given that sets up a list of registered customers.
A Feature is a placeholder for Scenarios, which what a namespace is to types, and a RegisteredCustomer is a simple data structure — so lets model that with a record type:
After the Given comes the When, which is the action we are specifying— actions are pretty simple to model as they are functions.
This When takes in a string Customer Id and a decimal Spend and we know it returns a decimal Total as that is asserted in the Then — lets call this type CalculateDiscount:
Not too sure I like these strings & decimals as we’re in design mode so lets pop in some aliases to make this more readable:
That’s a good start, but there’s a problem with our CalculateDiscount type. This entire scenario is about RegisteredCustomers receiving (or not) discounts — its literally in the scenario name, and yet our function signature knows nothing about RegisteredCustomers, only CustomerIds.
The scenario shows that the CustomerId is the key to the RegisteredCustomers list, and my Given creates a bunch of them so maybe we can just pass in a list of RegisteredCustomers (there’s only 3 of them after all)?
This would work I guess, but one of the key components of BDD is the collaborative specifying using Specification by Example or Example Mapping (or any other way of using examples to drive requirements).
As this scenario was specified collaboratively with the end users, product owners, business analysts, testers etc, we have a reasonable understanding of the existing system and we know that the registered customers is not a simple list as represented in the scenario, but rather a large relational database containing millions of rows.
Not sure passing all of the registered customers in as a list is the best design.
OK, well if we can’t use all of the registered customers, why not solve this with a function (this is F# after all)…lets pass in the ability to get a single registered customer instead.
Much better, but do we always get a RegisteredCustomer? What about Sarah? She hasn’t registered so won’t be in the database.
Not being registered is clearly not exceptional behaviour, it is expected (I “check out as Guest” all the time) — option types to the rescue.
The compiler will now enforce the business rule that not registering is not exceptional behaviour, saving us a test.
Again, this will work, but is it the best design? Functional programmers are always talking about purity, what about that?
We already know through collaborating with the current users of the existing system, that we will have to do some I/O from a database to get our customer which could make this impure.
Lets isolate that impurity into another type called GetRegisteredCustomer which will have the signature:
OK, well if we can’t use a list of all the customers as that’s impractical, and using a function could introduce impurity, then we’re left with just one choice — we’ll have to use a single RegisteredCustomer option in our CalculateDiscount type:
Perfect! We have a pipeline — the output from GetRegisteredCustomer feeds straight into CalculateDiscount.
With all our business rules in the (now) pure CalculateDiscount we have options when it comes to automating the scenario.
We could, for example; test the pure function CalculateDiscount directly, or test GetCustomer & CalculateDiscount together (maybe using an in-memory database), or even test our entire stack end-to-end.
As these behaviours drive development (BDD!), our automation suite should:
- provide confidence — a behaviour should only fail if the business logic it is specifying is either wrong, no longer required or has been implemented incorrectly
- give fast feedback — seconds not minutes
I try and pick an approach that meets both these requirements.
So there we have it — by taking a simple scenario that was specified collaboratively it has been pretty straightforward to convert that into an equally simple type system.
Using the scenario we have identified that “not being registered is not exceptional behaviour” and represented that using an option type.
Then we isolated our known I/O, so that all the business logic is captured in a pure function (which in turns gives us options for our automation) — all without having to write a line of implementation code.
Now that we have a set of concrete examples and basic type system, we can use standard TDD techniques with our favourite testing tool to drive out the details — but that’s for another day and a fresh pot of coffee.