Why We Need a Definition of BDD
Following on from the first post in this series - You’re Not Doing BDD - you might be wondering what the term BDD has meant all this time. Although it feels very much like swimming against the tide at this point, let me make a case for BDD as the term was originally intended. Let’s take a look at what real BDD is.
Basic Definition of BDD
BDD is a business process. It isn’t a kind of test. Back in 2016 when this stuff was gaining traction, there were a small number of practitioners who understood that there was a way to gather requirements, and it went like this:
- A conversation takes place between a (non-technical) domain specialist, a developer, and a QA
- Specific, objectively testable examples are gathered
- The developer may write tests reflecting these examples
- The developer will write classes and methods using ubiquitous language
- The QA can test the software using the objectively testable examples, and by other means
Features of a Good Feature
Scenario Example
Here is an example scenario, in Gherkin language:
Given a sandwich costs "£3.50"
And a banana costs "75p"
And a Diet Coke costs "£1.50"
And a meal deal is available
When I put a sandwich, a banana, and a Diet Coke in my basket
And I check the total price of my basket
Then I should See "£5"
What is in the Scenario
What is in the scenario is an objectively testable example. A QA might use it for a test plan. There is no confusion. No, ‘Ohhh! I thought you meant all three of the items get a 75p discount!’ moments. Just a clear example, and a clear answer to what output is expected.
What is also in the example, and this is important, is ubiquitous language. More on this later, but basically the domain language (‘basket’, ’total price’) used by the domain specialist is in the test. It will be in the code. It will be in the database column names. From this moment on, the word is ‘basket’, not ‘cart’. Language from our non-technical colleagues trickles through everything we do, and that will save headaches for our future friends and our future selves.
What is Not in the Scenario
There are some questions that you cannot answer from this scenario in isolation. The most important one is, what are we testing? Is this a test for a website? REST API? Mobile app? Is it a spec for an embedded system in a self checkout machine?
If you take nothing else away from this series of articles, remember this: if you can tell the answers to these questions just from the tests, then they are integration tests. They are not behavioural tests.
You also can’t tell what programming language we’re using. It’s not clear whether an ORM or a web framework is in play. SQL or document storage? Dunno.
It isn’t an integration test, because it isn’t about your tech stack. It’s about sandwiches.
Tip
If your domain is food shops, your integration tests may be about HTTP, but your behavioural tests should be about sandwiches.
After the Scenario: Step Definitions
Let us pretend that we have started to implement the above scenario. Here is an example step definition or two, written for PHP’s Behat:
/**
* @Given a sandwich costs :price
*/
public function aSandwichCosts(string $price)
{
$sandwich = \Supermarket\Domain\Item\Sandwich::fromPrice($price);
$this->db->persistItem($sandwich);
}
/**
* @When I check the total price of my basket
*/
public function iCheckTheTotalPriceOfMyBasket()
{
$this->basketTotalPrice = $this->basket->checkTotalPrice();
}
Let’s just take a moment to note that there are some very important things deliberately missing even now. You still can’t tell what kind of application this is for (web app, REST API etc.). You can’t tell if we are using an ORM, an MVC framework, even SQL/NoSQL. There is an important reason for this, and it becomes clear here:
BDD is about designing the domain.
The kind of application (CLI, HTTP etc.) is just that: the application layer. This is one of the three important parts of your software architecture, and it is not addressed here.
The infrastructure layer (Doctrine ORM, etc.) is also not mentioned here. This is another area that your behavioural tests should not touch on.
Only the domain is being designed. The domain is driving the design. I’m sure there’s a book or two about why that is good.
What we have is a design for the public API of your domain. We need nouns (i.e. classes) called things like Basket
. We
need verbs (methods) called things like checkTotalPrice
. Why? Because a person whose understanding of selling
sandwiches exceeded their understanding of software architecture told us so. Remember what that non-technical
stakeholder was saying in the meeting? That person was designing your domain for you.
What Am I Supposed To Do Now?
TDD, presumably. You’ve got all the classnames and method names you will need, so you’re off to a good start. Go write some unit tests.
What Does $db->persistItem()
Mean Then?
The fastest way for you to get these tests to pass (and that should be what you are looking for) is a Data Access Object interface, backed up by an in-memory implementation. Write an interface like this:
interface BasketDao
{
public function persistItem(\Supermarket\Domain\Item $item): void;
}
Then, write a fake in-memory implementation like this:
class InMemoryBasketDao implements BasketDao
{
private array $items = [];
public function persistItem(\Supermarket\Domain\Item $item): void
{
$this->items[] = $item;
}
}
The gist of it is that we are pretending there is a database behind this interface. However, for behavioural testing purposes, there is merely an in-memory array. This approach of coding against an interface, and worrying about the infrastructure layer after the domain is working, is characteristic of DDD and hexagonal architecture.
What Was the Point of All That?
Let’s take a look at the situation we are in, after we have written a domain that passes the test for this scenario.
We have an objectively testable example, direct from a domain specialist. We have a domain which provably implements the described behaviour. We have a codebase that uses the same ubiquitous language as the domain specialist who provided the requirements.
Importantly, we also have a portable domain. You can use your code for a web application. You can use it for a REST API. You can even put it inside a self-checkout machine, should the mood take you. It would work as well in a website as it would in any accompanying cronjobs. Such is the quality of the decoupling of domain and application layer, that we didn’t need to know where the domain was to be used until we’d finished building it. You don’t get that if you put HTTP verbs in the first test you write.