A short introduction to BDD
When I first learned BDD (behavior-driven development), I saw a graph in The RSpec Book that helped me understand the flow of BDD. I don’t have that book anymore, but I still remember what the graph looked like, and it’s so helpful! Let me show you what I remember.
First, let’s quickly touch on TDD (test-driven development) since it’s part of BDD. Think of TDD as a red-green-refactor cycle:
- Write a test that fails (red),
- Get the test to pass with the simplest implementation (green), and
- Refactor (keeping the test green).
BDD is similar, but we do TDD from the outside in. Think of it like two concentric circles:
We start writing tests from the outside – often called feature or acceptance tests. We use feature tests to describe the application’s behavior from the stakeholder’s perspective. That’s the outer circle.
The feature test can fail for outer-circle reasons or inner-circle reasons. While the outside test fails for outer-circle reasons, we keep fixing those issues until it fails for an inner-circle reason (e.g. missing behavior in our business logic). That’s when we step into the inner circle.
For the inner circle, we follow our regular TDD flow: write a test that fails (red), get the test to pass (green), and refactor.
Once we finish a red-green-refactor cycle for the inner circle, we step back out to the feature test to see if:
- (a) we have moved one step further in the feature test (and thus have another failure to step into), or
- (b) we have made the feature test pass (green). At that point, we refactor the whole feature.
In practice, there can be more concentric circles (depending on the boundaries in your system). But I always find there are at least two.
An abstract web example
When testing web applications, I typically define the outside circle as a feature test that asserts the behavior of my web app through libraries that use a web driver (e.g. using Wallaby in Elixir or Capybara in Ruby).
Suppose we run a feature test and hit these outer-circle failures:
-
A route we’re trying to visit doesn’t exist. We create that.
-
A controller doesn’t exist, or it doesn’t have that action defined. We create the missing pieces.
-
A variable doesn’t exist in the template we’re trying to render. We expose that through the controller.
We continue like that and rerun the test until we hit a failure that belongs to
the inner circle. Suppose our feature test fails because we tried to use a
non-existent Accounts.create(email, password)
function from the controller.
At that point, we drop into the inner circle and follow the TDD approach for that module and function:
- Write a test for
Account.create/2
that fails, - Write the implementation of
Account.create/2
, and - Refactor the function’s implementation.
Finally, we step back out to the feature test.
If the feature test fails for other reasons, we step back in and follow the TDD process again. But if the feature is green, we take the chance to refactor the whole feature. And voilà, we’re done! 🎉