What is BDD Testing? Behaviour-Driven Development Framework
⚡ Smart Summary
BDD Testing describes application behavior in plain Given-When-Then language so analysts, developers, and testers share one specification. This walkthrough applies that approach to REST API testing with Behave, the Python framework, covering setup, feature files, step implementations, execution, and reporting.
What is BDD (Behavior Driven Development) Testing?
BDD (Behavior-driven development) Testing is a technique of agile software development and is an extension of TDD, i.e., Test Driven Development. In BDD, test cases are written in a natural language that even non-programmers can read.
How BDD Testing works?
Consider you are assigned to create a Funds Transfer module in a Net Banking application.
There are multiple ways to test it:
- Fund Transfer should take place if there is enough balance in the source account
- Fund Transfer should take place if the destination a/c details are correct
- Fund Transfer should take place if the transaction password / RSA code / security authentication entered by the user is correct
- Fund Transfer should take place even if it is a Bank Holiday
- Fund Transfer should take place on a future date as set by the account holder
The Test Scenario becomes more elaborate and complex as we consider additional features such as transferring amount X for an interval of Y days or months, stopping a scheduled transfer when the total reaches Z, and so on.
The general tendency of developers is to develop features and write test code later. As is evident in the case above, Test Case development here is complex, so the developer will put off Testing until release, at which point quick but ineffective testing is performed.
To overcome this issue, Behavior Driven Development (BDD) was conceived. It makes the entire testing process easier for a developer.
In BDD, whatever you write must go into Given-When-Then steps. Let us consider the same example above in BDD:
Given that a fund transfer module in net banking application has been developed And I am accessing it with proper authentication When I shall transfer with enough balance in my source account Or I shall transfer on a Bank Holiday Or I shall transfer on a future date And destination a/c details are correct And transaction password / rsa code / security authentication for the transaction is correct And press or click send button Then amount must be transferred And the event will be logged in log file
This form is easy to write, read, and understand. It covers every possible test case for the fund transfer module and can be modified quickly to accommodate more. It also reads like living documentation for the module. Because BDD grew out of TDD, the difference between the two is worth settling before moving on to tooling.
BDD vs TDD: Key Differences
Both practices are test-first, and both shorten feedback loops. They differ in who writes the tests, what language those tests use, and which layer of the application they describe.
| Aspect | TDD (Test Driven Development) | BDD (Behavior Driven Development) |
|---|---|---|
| Focus | How a unit of code works internally | How the system behaves from the user perspective |
| Language | Programming language assertions | Natural language Given-When-Then scenarios |
| Primary author | Developer | Business analyst, product owner, tester, and developer together |
| Readable by non-programmers | No | Yes |
| Typical scope | Unit level | Feature, API, and acceptance level |
| Common tools | JUnit, pytest, NUnit | Behave, Cucumber, SpecFlow, JBehave |
The two are complementary rather than competing. Teams commonly keep TDD for unit-level design and add BDD scenarios on top to describe the behavior that stakeholders actually sign off. Since REST endpoints sit exactly at that acceptance layer, they are a natural fit for BDD.
What is REST API Testing?
As REST has become a popular style for building APIs, automating REST API test cases alongside UI test cases has become equally important. REST API testing involves testing CRUD (Create-Read-Update-Delete) actions with the methods POST, GET, PUT, and DELETE respectively.
What is Behave?
Behave is one of the popular Python BDD test frameworks. Here is how Behave functions:
- Feature files are written by your Business Analyst, Sponsor, or whoever owns the behavior scenarios. A feature file uses a natural language format describing a feature, or part of a feature, with representative examples of expected outcomes.
- These scenario steps are mapped to step implementations written in Python.
- Optionally, environmental controls run code before and after steps, scenarios, features, or the whole run.
With the roles of each file clear, the framework can now be installed.
Setting up BDD Testing Framework Behave on Windows
Installation:
- Download and install Python 3 from https://www.python.org/
- Execute the following command at the command prompt to install Behave
pip install behave- IDE: PyCharm Community Edition is used here — https://www.jetbrains.com/pycharm/download/
Project Setup:
- Create a new project
- Create the following directory structure
The screenshot above shows the layout Behave expects: a features directory holding the feature files, and a nested steps directory holding the Python implementations. Behave discovers both by name, so the folder names must match exactly.
Feature Files:
Now let us build the feature file Sample_REST_API_Testing.feature, with the feature being CRUD operations on the ‘posts’ service.
This example uses the https://jsonplaceholder.typicode.com/ posts sample REST service, a free fake API that accepts write requests and returns realistic responses without persisting changes.
Example POST scenario
Scenario: POST post example -> creating a new post item using the 'posts' service Given I set post posts API endpoint -> prerequisite: sets the URL of the posts service When I set HEADER param request content type as "application/json" And set request body And send POST HTTP request -> the actual test step Then I receive valid HTTP response code 201 And Response body "POST" is non-empty -> verification of the response body
Similarly, you can write the remaining scenarios as follows:
Sample_REST_API_Testing.feature
Feature: Test CRUD methods in Sample REST API testing framework Background: Given I set sample REST API url Scenario: POST post example Given I Set POST posts api endpoint When I Set HEADER param request content type as "application/json" And Set request Body And Send a POST HTTP request Then I receive valid HTTP response code 201 And Response BODY "POST" is non-empty Scenario: GET posts example Given I Set GET posts api endpoint "1" When I Set HEADER param request content type as "application/json" And Send GET HTTP request Then I receive valid HTTP response code 200 for "GET" And Response BODY "GET" is non-empty Scenario: UPDATE posts example Given I Set PUT posts api endpoint for "1" When I Set Update request Body And Send PUT HTTP request Then I receive valid HTTP response code 200 for "PUT" And Response BODY "PUT" is non-empty Scenario: DELETE posts example Given I Set DELETE posts api endpoint for "1" When I Send DELETE HTTP request Then I receive valid HTTP response code 200 for "DELETE"
Steps Implementation
Now, for the feature steps used in the scenarios above, you can write implementations in Python files inside the “steps” directory.
The Behave framework identifies the step function by matching decorators against the feature file predicate. For example, a Given predicate in a feature file scenario searches for a step function carrying the @given decorator. The same matching happens for When and Then. In the case of ‘But’ and ‘And’, the step function takes the same decorator as its preceding step. For example, if ‘And’ follows a Given, the matching step function decorator is @given.
For example, the When step for POST can be implemented as follows. Note how "application/json" is passed from the feature file into the braced placeholder — this is called parameterization.
# Decorator: the braced name captures a value from the feature file @when(u'I Set HEADER param request content type as "{header_content_type}"') def step_impl(context, header_content_type): # Step implementation: set the content type on the request header request_headers['Content-Type'] = header_content_type
Similarly, the implementation of the other steps in the step Python file will look like this:
sample_step_implementation.py
from behave import given, when, then, step import requests api_endpoints = {} request_headers = {} response_codes = {} response_texts = {} request_bodies = {} api_url = None @given(u'I set sample REST API url') def step_impl(context): global api_url api_url = 'https://jsonplaceholder.typicode.com' # START POST Scenario @given(u'I Set POST posts api endpoint') def step_impl(context): api_endpoints['POST_URL'] = api_url + '/posts' print('url :' + api_endpoints['POST_URL']) @when(u'I Set HEADER param request content type as "{header_content_type}"') def step_impl(context, header_content_type): request_headers['Content-Type'] = header_content_type # "And" or "But" steps are renamed by behave to match their preceding step @when(u'Set request Body') def step_impl(context): request_bodies['POST'] = {"title": "foo", "body": "bar", "userId": "1"} @when(u'Send POST HTTP request') def step_impl(context): # send the request and save the response object response = requests.post(url=api_endpoints['POST_URL'], json=request_bodies['POST'], headers=request_headers) response_texts['POST'] = response.text print("post response :" + response.text) response_codes['POST'] = response.status_code @then(u'I receive valid HTTP response code 201') def step_impl(context): print('Post rep code ;' + str(response_codes['POST'])) assert response_codes['POST'] == 201 # END POST Scenario # START GET Scenario @given(u'I Set GET posts api endpoint "{id}"') def step_impl(context, id): api_endpoints['GET_URL'] = api_url + '/posts/' + id print('url :' + api_endpoints['GET_URL']) @when(u'Send GET HTTP request') def step_impl(context): response = requests.get(url=api_endpoints['GET_URL'], headers=request_headers) response_texts['GET'] = response.text response_codes['GET'] = response.status_code @then(u'I receive valid HTTP response code 200 for "{request_name}"') def step_impl(context, request_name): print('Get rep code for ' + request_name + ':' + str(response_codes[request_name])) assert response_codes[request_name] == 200 @then(u'Response BODY "{request_name}" is non-empty') def step_impl(context, request_name): print('request_name: ' + request_name) print(response_texts) assert response_texts[request_name] is not None # END GET Scenario # START PUT/UPDATE @given(u'I Set PUT posts api endpoint for "{id}"') def step_impl(context, id): api_endpoints['PUT_URL'] = api_url + '/posts/' + id print('url :' + api_endpoints['PUT_URL']) @when(u'I Set Update request Body') def step_impl(context): request_bodies['PUT'] = {"title": "foo", "body": "bar", "userId": "1", "id": "1"} @when(u'Send PUT HTTP request') def step_impl(context): response = requests.put(url=api_endpoints['PUT_URL'], json=request_bodies['PUT'], headers=request_headers) response_texts['PUT'] = response.text print("update response :" + response.text) response_codes['PUT'] = response.status_code # END PUT/UPDATE # START DELETE @given(u'I Set DELETE posts api endpoint for "{id}"') def step_impl(context, id): api_endpoints['DELETE_URL'] = api_url + '/posts/' + id print('url :' + api_endpoints['DELETE_URL']) @when(u'I Send DELETE HTTP request') def step_impl(context): response = requests.delete(url=api_endpoints['DELETE_URL']) response_texts['DELETE'] = response.text print("DELETE response :" + response.text) response_codes['DELETE'] = response.status_code # END DELETE
⚠️ Note: Comparing status codes with == rather than is matters. The is operator tests object identity, not equality, and Python 3.8 and later raise a SyntaxWarning: “is” with a literal for that pattern. Any assertion written as assert code is 201 should be rewritten as assert code == 201.
Running the Tests
The test script development is complete, so let us run the tests. Execute the following command at the command prompt to run the feature file:
:: Run a single feature file with the pretty formatter behave -f pretty features\feature_files_folder\Sample_REST_API_Testing.feature :: Run every feature in the project behave
This displays the test execution results as follows:
Report display on the console
Console output is convenient during development, but stakeholders usually prefer a readable report, which Allure provides.
Reports
First, install the Allure Behave formatter and the Allure command line tool. The formatter is a Python package; the command line tool is a separate download described in the Allure Report documentation.
:: 1. Install the formatter pip install allure-behave :: 2. Run the tests and write raw results to a folder behave -f allure_behave.formatter:AllureFormatter -o reports/allure-results features/ :: 3. Render and open the HTML report allure serve reports/allure-results
This generates the test results report in a presentable and informative format like this:
Test Report in HTML Format
Test Report displaying individual Scenario result
A working suite is only useful if it stays readable as the API grows, which is where feature file discipline pays off.
Best Practices for Writing Behave Feature Files
A Behave suite degrades quickly when scenarios describe clicks and payloads instead of behavior. The practices below keep feature files readable for business stakeholders while keeping the Python layer maintainable for engineers.
- Describe behavior, not implementation. Write “Given a customer has sufficient balance”, not “Given the balance column equals 500”. The feature file states what the system does; the step file states how it is checked.
- Keep one behavior per scenario. A scenario that asserts a status code, a response body, and a database row is really three scenarios. Splitting them makes failures point directly at the cause.
- Move shared setup into Background. The example above uses
Background: Given I set sample REST API urlso that every scenario inherits the base URL without repeating it. - Parameterize instead of duplicating. Braced placeholders such as
"{request_name}"let one step function serve GET, PUT, and DELETE assertions, which is why the example needs far fewer step definitions than scenarios. - Use Scenario Outline for data variations. When the same behavior must be proved for several inputs, an
Examples:table is clearer than copied scenarios. - Avoid inter-scenario dependencies. Behave does not guarantee that a POST scenario runs before a GET scenario in every configuration. Each scenario should create the state it needs.
- Replace module-level globals with context. The example stores endpoints and responses in module dictionaries for brevity. In production suites, store them on Behave’s
contextobject so state resets cleanly between scenarios. - Tag scenarios for selective runs. Tags such as
@smokeor@regressionallowbehave --tags=@smoke, which keeps pipeline feedback fast.
Applying these habits from the first feature file keeps a BDD suite valuable well beyond the initial CRUD examples. Teams extending this approach further often pair it with Cucumber for JVM projects or Postman for exploratory API checks.







