Author: Deborah Emeni
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.
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:
Nodejs installed
Basic knowledge of JavaScript
Terminal
Code editor (Visual Studio Code editor)
To begin, create the project directory on your system by running this command in your terminal:
mkdir tdd-project
Next, change into the directory by running this command:
cd tdd-project
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
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
Once the installation is successful, jest will be installed and added to your package.json file. Replace your test script with this:
"test": "jest"
Your package.json file should look like this:
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);
});
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
After running that command, the following will be displayed in your terminal:
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;
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
If you implemented the subtract method correctly, then the test should pass as shown below:
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!
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:
The BDD workflow, as illustrated above, consists of several stages, which are explained below:
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.
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.
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.
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.
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.
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.
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.
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:
Code editor (Visual Studio Code editor)
Terminal
Basic knowledge of JavaScript
Nodejs installed
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
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
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
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
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
});
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"}\'',
};
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"
Then, run the tests with the following command:
npx cucumber-js
Once you run the command, the outcome should be displayed in your terminal as follows:
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:
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.