Today we bring yet another testing-related concept for you. Well, we’re bringing two: TDD (Test Driven Development) and BDD (Behavior Driven Development). These are two widely known software development techniques in which automated tests play a central role.
But people are often confused with these two concepts. “TDD vs. BDD?” is a question that gets asked a lot.
In today’s post, we’re going to give our contribution to this debate. In summary, this is what you’ll learn:
- What TDD is
- The TDD workflow
- Some different types of TDD
- A TDD example in JavaScript
- What BDD is
- The BDD workflow
- A BDD Example, also in JavaScript
After that diversion, we converge, by bringing the two together, summarizing the similarities and differences between them. Let’s get started.
TDD: A Quick Definition
TDD stands for Test-Driven Development. It’s a software development technique in which we write tests before writing the production code. Then, the developers must write the minimum amount of code necessary to make the tests pass. The idea is that, by strictly following this workflow, the tests will guide the design of the application to a more maintainable state.
How is that possible?
As you’ll see next, the type of automated tests we write when performing TDD has some interesting properties, among which the most important is their requirement for absolute—or as close to it as possible—isolation. When you do TDD you only write code in response to a failing test. That results in code that is as free of dependencies and coupling as possible.
That leads to simpler, better design, with code that’s easier to maintain and evolve.
TDD Is All About Unit Testing
Regarding TDD, when we talk about testing, we mean unit tests.
If you’re familiar with the concept known as the testing pyramid, then you know unit testing is one of the most important types of automated testing. Unit tests are small tests that test a small portion of the codebase—the unit—in complete isolation. We can also say that unit tests shouldn’t produce nor consume side effects.
What that means is that unit tests can’t interact with things that are external to the code, such as the database, the filesystem, the network, or even the system clock.
It’s also a unit test best practice having tests that don’t depend on one another, and also don’t require to be executed in any given order. In other words: unit tests, ideally, are totally independent and isolated from any external concern. When writing tests that exercise areas of the code that interact with such external concerns, you usually have to rely on techniques such as stubbing and mocking.
Unit Tests = Isolated Tests
One of the first benefits of isolation in tests that comes to mind is speed. Since unit tests don’t talk to the things we’ve just mentioned, they run way faster than tests that do interact with those dependencies—e.g. integration tests.
Other types of tests might require a complex and error-prone setup process—including steps like preparing an environment, creating or editing config files, or copying databases. Unit tests, on the other hand, oftentimes can be written with little or no setup at all.
But the most important benefit of unit test isolation has to do with determinism. A good unit test is deterministic. That is to say, if it’s failing, it should continue to fail, until something changes in the code—either the test itself or the system under test. The opposite is also true: if the test is passing, it should continue to pass, until something changes in either the test or the code being tested.
Test unit isolation is required to ensure that determinism. If a unit test depended on external systems, it could fail due to a problem in one of those systems. For instance, a test that relies on the database can fail due to a network outage that prevents a successful connection to the server.
The TDD Workflow
Robert C. Martin (aka Uncle Bob) describes TDD using what he calls The Three Laws of TDD:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
The three simple rules above define the whole workflow of TDD. However, we’re going to explain it in a more streamlined way.
Step #1: Write a Failing Test
The first step in the TDD workflow is to write a unit test for the piece of functionality you’re going to create. The test you’ll write will necessarily be a failing test since you’re trying to test something that doesn’t even exist. In other words, you’re going to be calling classes/methods/functions that you haven’t created yet. This will cause runtime errors when the test is executed.
In a compiled language such as C# or Java, the code won’t even compile.
Step #2: Execute All Existing Tests
The next step is to execute all executing tests. This step is crucial because it is essential to ensure all existing tests—if any—are passing. After running the tests, only the test you recently wrote should be failing.
Step #3: Write Production Code to Make the Test Pass
In this stage, you write the minimum amount of code possible to make the test pass. In other words: you shouldn’t necessarily try to solve the problem at this point. Rather, you should only aim at making the test pass. You’re not only allowed to “cheat”—by returning a hard-coded value, for instance. You’re encouraged to do so.
After writing the production code it’s time to execute the tests again. If your recently written test still doesn’t pass, you have to go back to step #3 and alter the production code in order to make sure it passes.
Step #4: Refactor If Necessary
At last, we get to the final and optional step where you refactor the code in order to remove code duplication and improve the code in general.
TDD Comes In Different Flavors
It’s important to stress that there are different styles or even different schools of thought when it comes to TDD. Those different styles might be referred to using a variety of names. For the purposes of this post, we’ll briefly cover two broad categories of TDD that are relevant to the topic of TDD vs. BDD.
Inside-Out TDD
The first TDD approach we’ll cover here is inside-out TDD, also known as bottom-up TDD, or Chicago School of TDD. As the name suggests, this approach works by starting with the internals of the application and working your way out.
When practicing this form of TDD, you start by writing lower levels unit tests. Then, you slowly progress to higher and higher levels, until you reach the layer of acceptance testing.
Here are some advantages of the inside-out approach:
- Comprehensive testing and coverage from the beginning.
- Parallel development i.e. developers can work independently on different features without much need for coordination.
And for the disadvantages:
- High-coupling of tests to low level implementation details.
- It might be too developer-centric, leading to code that doesn’t solve the user’s real needs.
Outside-In TDD
Outside-in TDD is also called Top-Down TDD. It’s sometimes referred to as the London School of TDD or Acceptance-Test-Driven-Development (ATDD.)
This approach, as the name suggests, is the opposite of the previous one. You start with the externals of the system and build your way app. This approach is way less developer-centric than the previous one. Instead of thinking about low-level implementation details, you’re more concerned with how a given piece of functionality works as a whole.
In this approach, you start at the acceptance-testing level, and then work your way in, by breaking down the requirements into smaller units.
Here are some of the benefits of this approach:
- Test cases less coupled with internal details, resulting in a less fragile test suite.
- Being less developer-centric, it’s more likely you’ll end up solving the user’s requirements.
For the cons:
- This approach might make it hard to do parallel development.
- This approach might require a greater deal of mocking/stubbing, to replace dependencies that aren’t ready yet.
A Brief TDD Example in JavaScript
We’re now going to see a quick TDD example, using JavaScript as the language. In our example, we’re going to start solving the String Calculator Kata, developed by Roy Osherov.
Here’s our first test:
var assert = require('assert'); var calc = require('../src/calculator.js') describe('StringCalculator', function() { describe('#add()', function() { var calc = new StringCalculator(); it('should return 0 when passing an empty string', function() { assert.equal(calc.add(''), 0); }); }); });
The test is super simple. It requires the calculator.js file, which exists, but it’s empty. Then, it verifies whether the add function really returns 0 when we pass an empty string. This test will fail spectacularly since we’re trying to test code that doesn’t exist.
The next step is to make the test pass. At this stage, you don’t need to try to get to the final solution. Instead, do the simplest possible thing for the test to pass. Here’s the code for src/calculator.js:
StringCalculator = function() {}; StringCalculator.prototype.add = function(stringNumbers) { return 0; }
As you see, we just return 0, and that makes the test pass.
The next phase is the refactor step, which is optional. Here, we do primarily two things:
- refactoring the code as we see fit (for instance, to remove duplication, to improve readability or efficiency)
- run the tests again, and they should all pass.
In the example, we don’t have a lot of room for improvement, so no refactoring this time.
BDD: A Quick Definition
Dan North introduced the concept of BDD with an article back in 2006. BDD is an acronym that stands for Behavior-Driven Development. Based on what you’ve just learned about TDD, you should be able to guess that it’s similar to TDD, but that the development is driven not by tests but by behavior. But what on Earth does “behavior” mean here?
We can think of a behavior as the way the application should…well, behave, to meet some requirements in order to serve a user’s needs.
BDD is a sort of extension or evolution of TDD that doesn’t get too bogged down by implementation details and technical matters, focusing instead on higher-level concerns, such as business requirements. It’s also all about communication. One of its most important and interesting characteristics is that it aims to encourage collaboration between developers, QA professionals, and business analysts, by providing a cohesive, ubiquitous language with which everyone can write and read requirements for the application.
BDD achieves that by using plain-English requirements as a starting point for the tests, allowing and encouraging non-technical participants to collaborate.
BDD Workflow
Let’s look at an example of a BDD workflow.
Step #1: Write the Behavior
The first step is to write the behavior we want to create. Ideally, people from the business—such as the product owner or a client representative, or business analysts—should write the behavior, in English, using the Given/When/Then template (more on that later.)
Step #2: Convert the Plain English Behaviors Into Scripts
The next step is to write programming tests based on the behaviors/requirements written in plain English.
Step #3: Implement the Behavior by Writing Production Code
At this step, we implement the required functionality by writing the production code and then run the behavior to verify whether everything is correct. If it’s not, we change the production code until the behavior tests pass.
Step #4: Refactor If Needed
Then we have the final, optional refactor step, same as with TDD. This stage is an opportunity for tidying up the code, removing duplication, unnecessary complexity, improving readability, and so on.
BDD Example in JavaScript
Time for our BDD example. As we did with the TDD example, we’re going to be walking through the workflow steps, showing how we’d do them in practice. For the BDD example, we’ll use Cucumber.
The examples we’re going to use will be based on the “10-minute tutorial” you can find on the Cucumber documentation site. Here we go.
Step #1: Write the Behavior
In Cucumber, you describe behaviors using scenarios. Consider the following code:
Feature: Is it Friday yet?
Everybody wants to know when it's Friday
Scenario: Sunday isn't Friday
Given today is Sunday
When I ask whether it's Friday yet
Then I should be told "Nope"
Here, we define a feature called “Is it Friday yet?” followed with a description. Then we define the scenario using the Given-When-Then syntax. The scenario simply defines that, if it’s Sunday and I ask whether it’s Friday, the answer should be negative.
The next step would be to convert the scenarios written in human-readable English to executable tests. The following excerpt of code shows the codification of the steps of our scenario:
const assert = require('assert'); const { Given, When, Then } = require('cucumber'); const isItFriday = today => { return 'Nope'; } Given('today is Sunday', () => { this.today = 'Sunday'; }); When('I ask whether it\'s Friday yet', () => { this.actualAnswer = isItFriday(this.today); }); Then('I should be told {string}', expectedAnswer => { assert.equal(this.actualAnswer, expectedAnswer); });
As you can see, we just hardcoded the response “nope” for the “isItFriday” constant. It’s going to make the test/behavior pass, even though it doesn’t fully solve the problem. At this step, the steps should pass.
TDD vs. BDD: All Together Now
It’s time to consider everything we’ve seen in this post and reach a conclusion. We’ll start by covering the main benefits of each methodology, starting with TDD. Then, we’ll discuss the similarities and differences between TDD and BDD. Then, we give our verdict on the debate. Let’s get to it.
TDD Main Benefits
As said before, neither TDD nor BDD are testing methodologies. Rather, they’re software development/design methodologies in which automated—unit—tests play a vital role.
That being said, the use of both approaches end up generating positive results for your testing approach. We’ll now see a summary of some of the main benefits you can get by adopting TDD.
- Simpler design. The TDD workflow encourages developers to only write the minimum amount of code necessary at all steps. That leads to a simple, minimalistic design, that doesn’t incur unnecessary couplings and dependencies, which makes the code simpler to navigate, maintain and refactor.
- Fewer defects. With TDD, you’re testing as early as you can, which leads to fewer bugs making it to production.
- Detailed project specification. Unit tests can play the role of detailed documentation for your project, and with TDD, you ensure it’s as comprehensive as it can be.
- High test coverage. Test coverage means ensuring you test the features that needed testing in your application. With TDD, you get that by default, since you write the test before writing the actual code.
- High code coverage. Code coverage is a more known metric, and it refers to the ratio of lines or paths in your codebase that is exercised by at least one unit test. With TDD, you get 100% unit test code coverage by default.
- Fewer regression problems. The comprehensive unit tests suite you get by adopting TDD servers as a safety net, protecting your app from regression problems.
BDD Main Benefits
BDD shares some of the benefits from TDD, while having a few exclusive ones. Let’s summarize them now:
- Collaboration. One of BDD’s main value propositions is to foster collaboration inside an organization by having technical and non-technical professionals participate in the creation of specifications.
- Fewer defects. As with TDD, teams adopting BDD also get the benefits of shift-left testing, which includes more defects being found—and fixed—before they make it all the way to production.
- Fewer regression problems. BDD also results in a comprehensive test suite that protects the application against regressions.
- Comprehensive project documentation fit for multiple target audiences. While the unit tests you write for TDD can be considered documentation, only developers can read them. BDD scenarios, on the other hand, are supposed to be written and understood by everybody. So, while both approaches generate comprehensive specifications, only BDD allows that specification to be leveraged by everyone in the organization.
- Traceability for every piece of functionality. The executable tests you write when doing BDD should link back to the scenarios on which they’re based. That way, you gain complete traceability: starting with any test, you can find out what are the requirements that originated it.
TDD vs. BDD: Similarities and Differences
We’re now going to summarize the main differences and similarities between the two approaches.
Let’s begin at the start. In TDD, the process starts by writing a failing test case. In BDD, you kick off the process by writing a scenario, in plain, human-readable English.
Both TDD and BDD aren’t testing methodologies—that’s a common misconception. They are software development methodologies that revolve around automated testing.
Both approaches achieve the result of having a comprehensive suite of automated tests covering the application, even though that’s certainly more pronounced with TDD.
Probably the starkest difference between the two techniques is the difference in regards to people producing and consuming the tests. TDD only requires the collaboration of developers. And even if non-technical people wanted to collaborate, they probably couldn’t, since reading TDD generated unit tests require programming knowledge.
BDD, on the other hand, encourages the collaboration of not only developers but also QA people, business analysts, and so on.
TDD vs. BDD: Key Differences, Summarized
TDD | BDD |
---|---|
TDD concerns more about the “how.” How is a certain functionality implemented? |
BDD is more concerned about the “what.” What is the end result, from the perspective of the user?
|
TDD is more “developer-centric”: the process starts by writing a failing unit test. | BDD is more end-user centric: the process starts by writing a scenario that describes how the application behaves in a given situation. |
The TDD process results in detailed documentation that only developers can read. | The BDD process generates detailed specifications in plain English, that every stakeholder can read. |
Only developers contribute to the TDD process. | BDD encourages and enables collaboration from non-technical stakeholders. |
The Final Verdict
Finally, the verdict. What is the answer to the “TDD vs. BDD” debate? Is there such a thing as the best approach?
The answer is that there isn’t a one-size-fits-all answer. There are scenarios where TDD is more indicated than BDD and vice-versa.
BDD might be the best approach for applications where the actions are dictated by the user’s behavior. On the other hand, for things like libraries or RESTful APIs, TDD might be the most suitable technique.
Back to You Now
In this post, we’ve examined the “TDD vs. BDD” debate, defining each of the two approaches along with examples. By now, you should have a solid understanding of the fundamentals of both approaches. Now, read more about both techniques and practice.
Don’t forget to stay tuned to this blog, where we constantly publish valuable content related to testing, such as this comprehensive tutorial on Cucumber.
This post was written by Carlos Schults. Carlos is a .NET software developer with experience in both desktop and web development, and he’s now trying his hand at mobile. He has a passion for writing clean and concise code, and he’s interested in practices that help you improve app health, such as code review, automated testing, and continuous build.