Behavior Driven Development & Testing
Test Driven means great!
The aim of Test-Driven Development (TDD) is to
minimize the number of mistakes in the code by forcing the developer to analyze
the test cases before writing the code. Its main value comes from engaging the
programmer in considering all the use cases before writing the production code.
The traditional
process of writing code using TDD involves
following steps:
- Plan what you want to do! (What’s given? What’s the result?)
- Write your test! (Arrange/Act/Assert)
- Realize the test fails…
- Make it green (by writing your code)
- Then follow the aforementioned steps until your functionality is implemented.
The obvious
drawback is that it takes time to think of the test cases, so the
implementation process is slower than writing the same code without TDD, which again is slower than
writing the same code without writing any tests, which is again slower than not
testing your code at all… There is a pattern to be seen there.
It may be
considered a heresy by some of the TDD enthusiasts,
but I’m convinced that implementation with TDD will always be
slower than one without it (that’s why people consider not using it). There’s
an investment one makes in hopes of writing more maintainable and stable code.
But…
I feel that
Test Driven Development is a tool
designed by programmers for programmers. By that I mean that its purpose lies
fully in maintaining code readability and quality. It prevents you from making
mistakes and from not considering all the technical cases. It doesn’t prevent
you, however, from writing working code that does wrong things. Having said
that, proving the validity of your understanding of the requirements is not the
goal of TDD, so it fully accomplishes
its purpose (It’s still great!).
Let’s assume
our team of developers got the following feature to be implemented:
As a client of a bank, I want to be able to
send an online bank transfer to another account.
For many of
you the first reaction probably was “well, that’s not very
precise…” or “ok, but what if…?”.
And that’s perfectly valid! Such questions lead to discussions which can be
resolved (and often are) by writing down the agreement in natural language, and
later attaching it to our requirements. This discussion goes on until all the
questions are answered and the answers are written down. After that, our
feature is ready to be implemented.
…the monster rears its
ugly head…
Unluckily
this part comes usually during implementation of said feature or, in the best
case, during some kind of detailed planning (in Scrum that would be Task
Planning). After the developer started the implementation, the realization
comes: the natural language doesn’t cover all the cases and it’s often unclear
what’s the expected outcome in a particular case. It’s already a problem, but
he can always reach the Project Manager/Product Owner and simply get the
answers. What could be worse, is if it’s unclear what cases are actually
covered. And if that situation occurs multiple times in one feature, it can
significantly slow the process down.
In our
example we can have a case defined as follows:
- If there’s money on the sender’s account, it will be transferred to the recipient’s account.
- If there’s not enough money on the sender’s account, it won’t be transferred to the recipient’s account.
That
particular example being fairly straightforward, you probably have already in
mind few problematic edge cases. But did you find them all (you could write
them down now)?
Behavior Driven
Development (BDD)
This part
could also be called “There’s no universal cure”. There are, however, methods
that allow us to minimize the probability of making a mistake. If you use TDD, you probably saw technical bugs
occurring despite using this methodic. It won’t fix all the problems, but it
will help you avoid them.
Now that we
got the elephant out of the room, let’s get to the meaty part.
BDD is in fact not so different from TDD, it just operates on a different
level. While TDD is supposed to help
you avoid technical mistakes and write clean code, BDD aims to support writing clean requirements and avoid
misunderstandings.
The method
focuses on defining clear business cases which will act as a basis for a
discussion on requirements. That means, unluckily, that we still have to do all
the work, but this time we’re hoping it will go just a bit easier. Let’s follow
the same pattern, as we do while writing unit tests (Arrange/Act/Assert):
(I kept the
key words in the example bolded)
Given following Bank
Accounts
Account Number
|
Saldo
|
FR12345
|
500 EUR
|
DE54321
|
400 EUR
|
When the following Transfer is requested between
the following Bank Accounts
Sender’s Account Number
|
Recipient’s Account Number
|
Transfer Amount
|
FR12345
|
DE54321
|
300 EUR
|
Then the following Bank Accounts have the following Saldo
Account Number
|
Saldo
|
FR12345
|
200 EUR
|
DE54321
|
700 EUR
|
Such
written requirement won’t solve all your problems, but it probably will help
you think about business cases before implementing the functionality. One use
case can help you think about many questions:
- What if the Transfer Amount is bigger than Saldo? What if the Transfer Amount is negative? What if Saldo is negative?
- What if the recipient and the sender is the same Bank Account? What if one of them is an invalid account number?
- What if I request multiple transactions?
- What if the transfer is unsuccessful? (look at the “transfer is requested”)
- What if two Bank Accounts are in different currency? What if there a difference between exchange rate at the time of request and execution of the transfer?
What
if…
I’m pretty
sure I didn’t list all the possible questions here, and those are only the
questions from a developer’s point of view – by that I mean we don’t question
the validity of the business case.
And what if
we question its validity? A business analyst may ask what happens, if there’s
not enough cash on the account, but there’s an open credit line. What if the
transfer has been requested, but it got cancelled before realization? What if
the recipient’s bank rejected the transaction for an unexpected reason? What
if…
The shown
method is called Specification by Example and it’s
the first and very important step of Behavior Driven Development.
Writing such tests is usually a teamwork between the client, business analysts
and technical specialists. Of course, to write useful tests, the whole group
needs to have a common understanding of terms and phrases used in the test
(what do we understand under the term “account”? What’s a “transfer”? When is
it “requested”?). To provide a common understanding, one must define a Domain Language. However, this
topic, as well as connected to it Domain Driven Design (DDD),
is enough to be covered in a separate article.
Next steps
Well
defined test cases can be later used as Living Documentation.
That means that they can be provided to the development team and used as a document
between the team and the client. Not only do they serve as clear requirements
for the project and precisely state what’s to be implemented, in such form they
can also serve as Integration Tests and Acceptance Tests.
The
proposed form of the test is not accidental and can be actually written and
automatized, using
Gherkins language
to define the test cases and a library (specific to the programming language
the team is using) to automatize them.
Conclusion
Practicing development using Specification
by Example and Behavioral Driven
Development helps to realize all the possibilities and helps ask the right
questions. It’s like exploring a new land – if there are no signs,
you still can find all the places and create a great map of the territory. It’s
just a little easier when there are signposts, listing all the possible
directions on the crossings.
good article..surely makes my concept clear about TDD and BDD
ReplyDeleteThank you! I'm glad you find the article helpful :)
Delete