DEV Community

Cover image for TDD vs BDD - A Detailed Guide
Necati Özmen for Refine

Posted on • Originally published at refine.dev

TDD vs BDD - A Detailed Guide

Author: Deborah Emeni

TDD vs BDD

Software testing is critical in the software development cycle and ensures that the developed products are reliable and of high quality. The quality of the application is vital for providing a satisfying user experience. Paying attention to the development methodology used in writing tests in an application is also important.

Test-Driven Development (TDD) and Behavior Driven Development (BDD) are two popular and effective methodologies developers use to write quality tests that benefit developers, users, product managers and stakeholders.

In this article, you’ll learn about Test-Driven Development (TDD) and Behavior Driven Development (BDD), including what they entail, their principles, advantages, disadvantages, how they work and their key differences.

Steps we'll cover:

Overview of Test-Driven Development

TDD is a repetitive and continuous process based on agile development methodology that involves creating test cases at each stage of developing an application to define the expected code behaviour.

In TDD, developers first create a unit test case to showcase the desired behaviour of the code before actually implementing it. If the test fails, they iteratively write new code until it successfully passes. Afterwards, they proceed to refactor the application's source code, which involves restructuring the code without introducing new features or compromising the original functionality of the application.

To implement TDD effectively, the process entails breaking down the application's functions and generating tests for each aspect. This approach ensures systematic and thorough testing and monitoring of the components.

A good example of TDD can be seen in building an Authentication system in an application.

tdd description

According to the illustration above, the developer begins by identifying and defining the authentication system's requirements, including authentication methods such as OAuth, username, password, etc. The developer then writes a test that defines the expected behaviour for one of the authentication system's components, such as the login functionality.

After that, the developer would run the tests, which would initially fail because the functionality had not yet been implemented. The developer then writes the code necessary to pass the test.
The tests are then re-run, and the code is refactored. After refactoring, the tests are rerun to ensure they continue to pass.

After the login functionality has been validated, additional test cases for other functions, such as account verification, registration, and password reset, are created, and the TDD process is repeated.

Pros and cons of TDD

TDD offer several benefits. Still, it also has some drawbacks, as seen below:

  • Faster Development Cycle: TDD allows for the continuous delivery of software updates, and its architecture enables developers to quickly identify and fix bugs in their code. The rapid integration of updates promotes faster development and the delivery of high-quality software.

  • Improved Code quality: Writing test cases before writing the code enables developers to understand the desired functionality better and write well-structured code. Also, using the TDD approach makes it easier to refractor the section of the code and make it less buggy without affecting the existing functionality.

  • Time-consuming: TDD requires more time and effort in writing test cases before implementing the functionality, which may slow down the development process for projects with limited resources and short deadlines.

  • Rigid: The TDD approach of writing tests before implementing code is rigid because it is unsuitable for complex projects with constantly changing requirements.

Step-by-step demo example of TDD implementation

Let's see how TDD works in practice by building a simple app.

Prerequisites

To follow along with the tutorial, ensure you have the following:

To begin, create the project directory on your system by running this command in your terminal:

mkdir tdd-project
Enter fullscreen mode Exit fullscreen mode

Next, change into the directory by running this command:

cd tdd-project
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor, and in your project’s directory, run the following command to initialize a new Node.js project:

npm init
Enter fullscreen mode Exit fullscreen mode

Next, you need to install a testing framework that will be used for performing unit testing in your project. Several testing frameworks are available depending on the programming language used to create an application. For example, JUnit is commonly used for Java apps, pytest for Python apps, NUnit for .NET apps, Jest for JavaScript apps, and so on. We’ll use the Jest framework for this tutorial since we are using JavaScript.

To install the Jest testing framework as a dev dependency in your project's directory, simply run the following command:

npm install jest --save-dev
Enter fullscreen mode Exit fullscreen mode

Once the installation is successful, jest will be installed and added to your package.json file. Replace your test script with this:

"test": "jest"
Enter fullscreen mode Exit fullscreen mode

Your package.json file should look like this:

kson description

Using the TDD approach to build your application demands that you start by writing the tests. Create a file called sub.test.js in the root directory of the tdd-project that will contain the tests. Jest uses a .test.js naming convention for files, so ensure your file has that extension.

Now, you can start writing your tests. Let’s say you want to create a small calculator app, and the first functionality you’d like to implement is the subtraction function. The Jest framework has its unique way of writing tests as defined in the documentation.

Jest uses a test() function, which accepts a description as the first argument where you can describe the behaviour you want to test and a callback where you can use an expect() function and a toBe() matcher that lets you define the expected behaviour of your code and check if the behaviour matches those expectations.

Let’s see the test() function in practice. In your sub.test.js file, add the following test that will define the behaviour of subtracting values with the subtract method that you will define later in the code:

const Calc = require('./calc');

test('subtraction', () => {
  const calc = new Calc();
  const sub = calc.subtract(20, 10);
  expect(sub).toBe(10);
});
Enter fullscreen mode Exit fullscreen mode

Next, we’ll try running the test, which will fail because you haven’t written the functionality yet. However, this is an essential step as the test failing shows that the test is testing the behaviour.

Run the test with this command:

npm test
Enter fullscreen mode Exit fullscreen mode

After running that command, the following will be displayed in your terminal:

alt description

Next, let’s write the code that implements the functionality. In your project’s root directory, create a file called calc.js and add the following code:

class Calc {

  subtract(x, y) {
    return x - y;
  }
}

module.exports = Calc;
Enter fullscreen mode Exit fullscreen mode

Here, we are creating a class called Calc and adding a subtract() method for the values we defined in the test case. Then we are exporting the class to use it outside of this module.

Now, you can rerun the test with this command:

npm test
Enter fullscreen mode Exit fullscreen mode

If you implemented the subtract method correctly, then the test should pass as shown below:

success description

You have successfully written your first test case for one functionality in your application. If your test case fails, you can correct and refactor your code. Then, you can write and run more test cases for other functionalities like sum, average, division and more.

Next, we’ll look at Behavior-Driven Development (BDD)


Open-source enterprise application platform for serious web developers

refine.new enables you to create React-based, headless UI enterprise applications within your browser that you can preview, tweak and download instantly.

🚀 By visually combining options for your preferred ✨ React platform, ✨ UI framework, ✨ backend connector, and ✨ auth provider; you can create tailor-made architectures for your project in seconds. It feels like having access to thousands of project templates at your fingertips, allowing you to choose the one that best suits your needs!


refine blog logo

Overview of Behavior-Driven Development

BDD is another agile-based development process for creating tests that describe an application's expected behaviour based on users’ expectations. Compared to TDD, BDD focuses on meeting business needs and user requirements rather than simply passing tests.

With BDD, developers can create products focused on meeting users' needs based on their interactions with the product. The BDD approach encourages collaboration between product managers (usually in charge of defining the product's requirements), developers and testers.

In BDD, developers can use testing tools such as Cucumber, SpecFlow, Behave, and others to plan and write tests in a language known as Gherkin, which helps define the product's business requirements or specifications in a structured format using keywords in human-readable syntax.

See an illustration of the BDD workflow below:

bdd description

The BDD workflow, as illustrated above, consists of several stages, which are explained below:

  1. Identifying User Features: This is the first stage in BDD where the features that need to be developed are identified. The features are described here based on the users' expectations.

  2. Create Feature files: This stage entails creating files to document the application's features in a structured format that developers, product teams, and testers can understand using the Gherkin language.

  3. Writing Scenarios: At this stage, test cases are defined in feature files with examples describing the expected behavior of the feature. The Gherkin language has a syntax for defining test cases.

  4. Team Assessment:This is the stage at which the developers, product team, and testers collaborate to evaluate the feature files and scenarios created and defined in previous stages. The evaluation is performed to ensure that the defined scenarios align with the business requirements and the expectations of the users.

  5. Writing Step Implementations: At this point, the implementation of the scenarios described in the Gherkin language begins. Developers write code in a specific programming language (e.g., JavaScript or Java) that depends on the BDD framework to map each step in the scenario to the corresponding actions that must be executed.

  6. Test Automation: After the steps defined in the scenarios are implemented, automated tests are written to run the scenarios by simulating user interactions with the application and determining whether the behaviour matches the specifications in the scenarios.

  7. Test Validation and Reporting: At this stage, the automated tests are run, and the outcome of the scenarios (whether fail or pass) are recorded for the developers, product team, and testers to review.

  8. Continuous Development: As developers receive new requirements from users or the product team, updates are made to the feature files and scenarios, and the entire cycle (i.e. the previous BDD stages) is repeated until the expected behaviour is achieved.

Pros and cons of BDD

BDD has several pros as well as cons. Here are a few:

  • Building Customer-centric Products: Products developed using the BDD approach are customer-centric because most of the features implemented are based on customer feedback. As a result, BDD ensures that the products align with and meet the customers' expectations.

  • Foster Collaboration and Transparency: The BDD approach provides transparency for developers, product teams, and testers to collaborate and understand the features defined, ensuring that they align with business requirements and user expectations.

  • Feedback-based: BDD depends on clear and effective communication between users and developers. When the communication channel is disrupted, the feature development process is hampered by a lack of collaboration between users and developers.

Step-by-step demo example of BDD implementation

In this example, you’ll learn how to create tests using the BDD approach.

Prerequisites

To follow along with this tutorial, you’ll need the following:

Let’s start by creating a directory for the project. Open your terminal and run the following command to create a folder called bdd-project:

mkdir bdd-project
Enter fullscreen mode Exit fullscreen mode

Open the project in your code editor. In the root project’s directory, run the following command to initialize a new Node.js project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Cucumber.js works with Node.js and is available as an npm module, so you’ll use it as your testing framework. Within your project directory, run the following command to install Cucumber.js as a development dependency:

npm install --save-dev @cucumber/cucumber
Enter fullscreen mode Exit fullscreen mode

Next, create a folder called features with a file called auth.feature (the .feature extension is compulsory) that will contain the scenarios you’ll define.
In the auth.feature file, you’ll use keywords provided by the Gherkin Syntax to describe the behaviour of logging into the application from the users’ perspective as follows:

Feature: Login feature
As a customer
I would like to log into the application
So that I can gain access to my account

Scenario: Successful login
Given I am at the login page
When I type in my correct username and password
And click the "Login" button
Then I should be redirected to my home page
Enter fullscreen mode Exit fullscreen mode

The keywords used above are explained below:

  • Feature: The Feature keyword is the first keyword that describes the feature in a short text.
  • Scenario: This Scenario keyword defines the specific test case that describes a particular behaviour of the login feature.
  • Given: The Given keyword specifies the initial state of the scenario.
  • When: The When keyword describes the action performed by the user on the login feature.
  • And: The And keyword adds more steps to the scenario that describe an action carried out by the user
  • Then: The Then keyword specifies the expected outcome after the previous steps have been executed in this scenario

So far, you have defined steps in your feature file, next you’ll need to map the steps to their respective code implementation. To do this, create a file called step_implement.js and paste the following code:

const { Given, When, Then, And } = require('@cucumber/cucumber');

Given('I am at the login page', function () {
  // write code that navigates to the login page
});

When('I type in my correct username and password', function () {
  // write code to enter valid user credentials
});

When('click the {string} button', function (buttonText) {
  // write code to click on the specified button
});

Then('I should be redirected to my home page', function () {
  // write code to verify the redirection to the home page
});
Enter fullscreen mode Exit fullscreen mode

You're importing the Given, When, Then, and And keywords from the Cucumber npm module you installed and writing the code to implement all of the steps and actions defined in the Scenario.

To execute the steps defined in the feature file, create another file called configure.js that will contain the Cucumber.js configuration:

module.exports = {
  default: '--format-options \'{"snippetInterface": "synchronous"}\'',
};
Enter fullscreen mode Exit fullscreen mode

You're configuring the default options for the Cucumber.js test runner here by specifying the format of the output generated by Cucumber to synchronous.

In your package.json file, set your test script to this:

"test": "cucumber-js"
Enter fullscreen mode Exit fullscreen mode

Then, run the tests with the following command:

npx cucumber-js
Enter fullscreen mode Exit fullscreen mode

Once you run the command, the outcome should be displayed in your terminal as follows:

cucember description

The tests will pass once you add the appropriate code to implement the scenarios.

Comparison of TDD and BDD

So far, you've learned what TDD and BDD are, what they entail, and how they work. Let's look at how they differ in various aspects, as shown in the table below:

comparinf description

Conclusion

Finally, you've reached the end of this article, where you learned about Test-Driven Development (TDD) and Behavior-Driven Development (BDD), including what they entail, their principles, their benefits and drawbacks, and how they differ. You also saw TDD and BDD in action in a demo application.

Top comments (10)

Collapse
 
samuelfaure profile image
Samuel-Zacharie FAURE • Edited

I have to say I respectfully definitely do not agree. I read the article you wrote about TDD considered harmful, and I've seen this as criticism:

For any non-trivial project, any intermediate experienced programmer can tell you writing tests before you write the code as a way to design the code is a non-starter

Which I definitely do not agree with, as an experienced programmer; Not only would I say that TDD forces you to design simple, efficient code, and makes refactoring dead easy,I would also say that TDD is absolutely not for beginners, but for experienced programmers only (This is such a common misconception that I'm currently writing a draft on this).

TDD is actually extremely difficult and very misunderstood in my opinion.

I also do not agree with:

TDD encourages writing code you will not need, that gets thrown away, never used or worse, hangs around in your code base as "zombie" code whose only function is to make you feel good about not getting anything useful actually done.

On the opposite, TDD (with some BDD thrown in it) allows the removal of dead code dead-easily. I don't even understand where this criticism is coming from.

I do agree with your observation that requiring 100% test coverage is not a good idea. In my experience, 80-95% is the sweet spot, depending on your product maturity.

With your permission, I will tag you on my future article for an exchange of constructive criticism.

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch

This looks like an either-or decision, but it's not. TDD and BDD reside on different abstraction levels and you can do both at the same time. But should you?

I guess no-one questions the benefits of unit tests, so you only should ask yourself whether to write them in a TDD approach or not. They make sure that your code works. You don't know if it does the right thing, but you know it does *some*thing.

BDD as described in this article is a way to document user requirements in a programmatic way. While I appreciate the pursuit of a structured and automated process to gather user approval I unfortunately had to realise that BDD is a waste of time. There is not much benefit in having user requirements as executable Cucumber code over user stories or less structured ways of documenting requirements. I would even go as far and say it's harmful, because it gives you (false) confidence in your code. But your users aren't any more likely to approve your code just by having another layer of code that runs successful. Real user approval can only be reached by interacting with your user - not with your code.

Collapse
 
dyaskur profile image
Muhammad Dyas Yaskur

Can't we use both?

Collapse
 
sebastian_wessel profile image
Sebastian Wessel

You should, and most of the time you do use both.

Unit tests are most of the time the TDD part and integration tests are more BDD.
Also, the regular workflow with stories and implementing features is more BDD oriented, while the actual technical implementation is more TDD.

Both are valid, and they are not on the same abstraction level in most cases.
TDD is more lower level in the test Pyramide, while BDD is on top of it.

 
samuelfaure profile image
Samuel-Zacharie FAURE

neither property is anything anyone should be promoting much both together

Despite being difficult, it is still imho extremely worth it, even for a beginner, in most situations (not all situations I'll agree). I will detail more about that when I'll have the time to finish that article.

Thread Thread
 
justcodehj profile image
Hemanshu Jamvecha

I have some fools asking me to write test case for DTOs in JAVA

Collapse
 
starkraving profile image
Mike Ritchie

I routinely use TDD for bug fixes. I normally write my tests after a new feature is developed, but when I’m fixing an issue that’s been reported, writing the test first helps me make sure that I understand what went wrong. If I can write a new test that fails while the original ones continue to pass, I’m probably on to something. Then I write the bug fix, and if I can get the new test to pass without disrupting the original tests, I can proceed.

To claim in a blanket statement that TDD is fantasy and ridiculous is unfair. I’m not a consultant looking to pad my invoice; I’m a conscientious developer who wants to have confidence that I understand what went wrong and how to resolve it.

Collapse
 
elsyng profile image
Ellis • Edited

Harsh, but I personally agree at least 50% with Jarrod. I've never seen any metrics about the pros AND cons of automated tests (tho I am not claiming they don't exist). People just claim things. Methodology or mythology? :o)

Not directly related, but I'd like to add that I've never seen anybody discuss/mention that the benefits of automated tests for frontend versus backend can be very different.

I like the comparison chart, though, it's useful, thanks. :)

Collapse
 
fitrapujo profile image
Nurfitra Pujo

it doesn't benefit you doesn't mean it would not benefit others

Collapse
 
justcodehj profile image
Hemanshu Jamvecha

Sorry Guys, mine one is just out of the box - I dont know how to write efficient Test cases! Does anyone has any great ideas or way of learning which they personally implemented to master it ?
Thanks