This article aims to give a brief explanation about what Behaviour-Driven Development (BDD) is and how it can be used to fill the information gap between stakeholders and development teams, ensuring everyone (technical or not) is involved in the project’s progress.
The first time I had contact with this approach of software development was a few years ago during a Massive Open Online Course (MOOC) and, I must warn you, I've never applied it in production. Nevertheless, I'm fully aware this is not the holy grail and it might not work so well as advertised but, the idea of having everyone in the team (not only developers) collaborating on the development process, sounded very romantic to me.
In the past few months, I've been working with Scala and I've been thinking about what would be the best way to apply BDD using the most popular testing tools in the Scala environment (ScalaTest and Specs2) and the most popular BDD tool: Cucumber. In this article I will compare these three tools using a very simple example.
What's BDD?
BDD can be described, in very simplistic terms, as an extension for Test-Driven Development (TDD).
The typical TDD cycle, aka Red/Green/Refactor, starts with the development of a failing test. The next step is to write the code to make this test pass and finally, refactor the code in order to improve its readability and maintainability.
In comparison, the BDD cycle is almost identical to the latter except the first step is replaced by the writing of a specification, rather than the development of a test. Meaning this first step can be done by a non-technical person, for instance a member of the business team or a functional analyst with an understanding of how the system should behave. The best part is this specification can be used for tests automation in the development process.
BDD can also be described as the intersection between Domain-Driven Design (DDD) and TDD.
Domain-Driven design is an approach to software development aiming to deal with projects with complex business rules where collaboration between developers and domain experts (typically, this role is assumed by the client) is necessary to set a common language and a domain model that can be translated into very specific and detailed requirements. In BDD, this specific requirements are used to drive the development process in the first step of its cycle.
BDD in practice
Obviously, BDD can be put into practice without using a specific tool or framework, you just have to get people to collaborate, record that collaboration in some form of specification, and then automate that specification to drive out the implementation. However, there are tools that can help significantly in the automation process, so we don't have to find a new way to extract values from the requirements to automate tests.
Cucumber
Cucumber is probably the most popular tool when it comes to BDD and it was the first one I worked with. It is advertised as being capable to ease tests automation using executable specifications and therefore, generating living documentation that is easily understood by everyone.
It was originally developed in Ruby, but it is also officially available for Java and Javascript.
The specifications are written in Gherkin, a simple set of grammar rules generating plain text structured enough for Cucumber to understand. Here's a simple example of a Bank Account feature specification from where the holder can withdraw cash:
Feature: Account Holder withdraws cash
Scenario: The account has sufficient funds
Given an account with a balance of $100.5
When the Account Holder requests $20.25
Then the account balance should be $80.25
Scenario: The account has insufficient funds
Given an account with a balance of $80.25
When the Account Holder requests $100.75
Then the Account Holder should be notified that overdrafts are not permitted
Gherkin documents are stored in .feature
text files and it supports different dialects, so you can even write your specifications in Esperanto!
The Scala implementation for the previous feature is something like this:
class BankAccount(val balance: Double) {
def debit(amount: Double): Either[String, BankAccount] = {
if (amount < 0.0) {
Left(s"The debit amount must be > 0.0")
} else if (balance - amount < 0.0) {
Left("Overdrafts are not permitted")
} else {
Right(new BankAccount(balance - amount))
}
}
def credit(amount: Double): Either[String, BankAccount] = {
if (amount < 0.0) {
Left("The credit amount must be > 0.0")
} else {
Right(new BankAccount(balance + amount))
}
}
}
I believe the code doesn't require any detailed explanation. It basically enables the creation of a BankAccount
instance with a given amount of cash that can be debited and credited, but overdrafts are forbidden.
Using this library I managed to automate the execution of the previous feature in a Scala project. It looks like this:
class StepDefinitions extends ScalaDsl with EN {
var bankAccount: BankAccount = _
var result: Either[String, BankAccount] = _
Given("""^an account with a balance of \$(.+)$""") { (balance: Double) =>
bankAccount = new BankAccount(balance)
}
When("""^the Account Holder requests \$(.+)$""") { (amount: Double) =>
result = bankAccount.debit(amount)
}
Then("""^the account balance should be \$(.+)$""") { (expectedResult: Double) =>
assert(result.right.exists(_.balance == expectedResult))
}
Then("""^the Account Holder should be notified that overdrafts are not permitted$""") { () =>
assert(result.left.exists(_.equalsIgnoreCase("overdrafts are not permitted")))
}
}
Basically, we're just extracting the values from each one of the specification sentences and doing operations and assertions with those values.
As you might have noticed (codacy obviously noticed it), I had to sacrifice immutability, one of the building blocks of functional programming, by using two mutable variables to be able to do the assertions in the last steps. It would be cleaner if each step could return a value to be used in the next steps and do the assertions in the end.
I wasn't the only one that noticed this limitation and some issues were already reported to make this more functional. However, it seems these improvements won't happen in the near future as it was announced the support for JVM languages was dropped, and this obviously includes Scala.
ScalaTest
ScalaTest is probably the most popular testing tool in the Scala ecosystem. It provides some traits facilitating BDD style, enabling a more grammatical structure in order to write tests as specifications.
The tests for the BankAccount implementation previously shown looks like this:
class BankAccountSpecs extends FeatureSpec with GivenWhenThen {
feature("Account Holder withdraws cash") {
scenario("The account has sufficient funds") {
val balance = 100.5
Given(s"""an account with a balance of $$$balance""")
val bankAccount = new BankAccount(balance)
val withdrawAmount = 20.25
When(s"the Account Holder requests $$$withdrawAmount")
val resultBankAccount = bankAccount.debit(withdrawAmount)
val expectedBalance = 80.25
Then(s"the account balance should be $$$expectedBalance")
assert(resultBankAccount.right.exists(_.balance == expectedBalance))
}
scenario("The account has insufficient funds") {
val balance = 80.25
Given(s"""an account with a balance of $$$balance""")
val bankAccount = new BankAccount(balance)
val withdrawAmount = 100.75
When(s"the Account Holder requests $$$withdrawAmount")
val resultBankAccount = bankAccount.debit(withdrawAmount)
val expectedMessage = "overdrafts are not permitted"
Then(s"the Account Holder should be notified that $expectedMessage")
assert(resultBankAccount.left.exists(_.equalsIgnoreCase(expectedMessage)))
}
}
}
The main difference I noticed when using ScalaTest compared with Cucumber is the specification text is inlined with the code and the Given/When/Then statements are basically just printed logs to the console, giving a greater level of granularity than just the test names. Therefore, the advantage of having the specification completely separated from the code is lost.
Specs2
Specs2 is another popular testing tool in the Scala ecosystem and it's my favourite one. It also provides a trait enabling the BDD style of using specifications for tests and, in my opinion, this is the best option to practice BDD in Scala. Here's the code for the successfull withdraw scenario:
class BankAccountWithSufficientFunds extends Specification with GWT {
def is =
s2"""
Given an account with a balance of $$100.5 ${bankAccount.start}
When the Account Holder requests $$20.25
Then the account balance should be $$80.25 ${bankAccount.end}
"""
private val bankAccount =
Scenario("bankAccount")
.given(aBankAccount)
.when(aDouble) { case debit :: bAccount :: _ => bAccount.debit(debit) }
.andThen(aDouble) {
case expectedBalance :: bAccount :: _ =>
bAccount.right.exists(_.balance == expectedBalance)
}
}
As it is visible, the specification text is also not completely separated from the code but it is not inlined. It is in a separated string that could be easily extracted from some other place. In this string, we have to specify where the scenario starts, where it ends, and every line between these two points must correspond to a given
, when
or andThen
call on the scenario.
We also have to write some custom step parsers for the balance and withdraw values and to instantiate the BankAccount
when the values are extracted from the specification string. Here's the code:
object CustomStepParsers extends StepParsers {
private val regex = ".*\\$(.*)$"
def aDouble: StepParser[Double] = readAs(regex).and((s: String) => s.toDouble)
def aBankAccount: StepParser[BankAccount] =
readAs(regex).and((s: String) => new BankAccount(s.toDouble))
}
And finally, the code for an unsuccessfull withdraw operation, where the account holder is notified overdrafts are not permitted:
class BankAccountWithInsufficientFunds extends Specification with GWT {
def is =
s2"""
Given an account with a balance of $$80.25 ${bankAccount.start}
When the Account Holder requests $$100.75
Then the Account Holder should be notified that overdrafts are not permitted ${bankAccount.end}
"""
private val bankAccount =
Scenario("A Bank Account with Insufficient Funds")
.given(aBankAccount)
.when(aDouble) { case debit :: bAccount :: _ => bAccount.debit(debit) }
.andThen() {
case insufficientFunds :: bAccount :: _ =>
bAccount.left.exists(msg => insufficientFunds.toLowerCase.contains(msg.toLowerCase))
}
}
Conclusion
I think BDD, if correctly applied and with everyone commited, can be a very efficient approach to software development. Especially for projects where a significant number of team members don't have a deep understanding of the business logic. Having a way to formally specify what the system should do and how it should behave and use this specification for testing and documentation, can avoid many issues during the development process.
About the tools comparison, despite Cucumber being the standard tool to practice BDD, I think Specs2 was definetely the best testing tool for this simple example in Scala.
The project with the code used for this blog post is available in GitHub, feel free to check it out.
See the original article here
Top comments (2)
Small suggestion: expand the acronym at some point, preferably near the beginning. Only the Venn diagram explains what the B stands for.
Hi Pablo, thanks for the feedback. I edited the post to expand the acronym at its first occurrence.