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.

  • 🗣️ Core Principle: Behavior-Driven Development extends Test-Driven Development by expressing every scenario in natural language that non-programmers can read and approve.
  • 🧱 Scenario Structure: Given establishes preconditions, When performs the action, and Then asserts the outcome, while And or But inherit the decorator of the preceding step.
  • 🌐 REST Coverage: The example exercises full CRUD behavior through POST, GET, PUT, and DELETE against the public jsonplaceholder posts service.
  • 🐍 Framework Setup: Behave installs through pip on Python 3 and expects a features directory containing feature files plus a steps package holding the implementations.
  • 🔗 Step Binding: Decorators such as @given, @when, and @then match the feature text, and braced placeholders pass values from the scenario into the Python function.
  • ▶️ Execution: The pretty formatter prints a colour-coded pass or fail result for every step in the console.
  • 📊 Reporting: The allure-behave formatter writes machine-readable results that the Allure command line renders as a browsable HTML report.

What is BDD Testing

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:

  1. Fund Transfer should take place if there is enough balance in the source account
  2. Fund Transfer should take place if the destination a/c details are correct
  3. Fund Transfer should take place if the transaction password / RSA code / security authentication entered by the user is correct
  4. Fund Transfer should take place even if it is a Bank Holiday
  5. 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:

Project Setup:

  • Create a new project
  • Create the following directory structure

Project Setup

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:

Project Setup

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:

Steps Implementation

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:

Running The Tests

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:

Reports

Test Report in HTML Format

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.

  1. 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.
  2. 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.
  3. Move shared setup into Background. The example above uses Background: Given I set sample REST API url so that every scenario inherits the base URL without repeating it.
  4. 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.
  5. Use Scenario Outline for data variations. When the same behavior must be proved for several inputs, an Examples: table is clearer than copied scenarios.
  6. 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.
  7. 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 context object so state resets cleanly between scenarios.
  8. Tag scenarios for selective runs. Tags such as @smoke or @regression allow behave --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.

FAQs

Both read Gherkin feature files. Cucumber binds steps to Java, Ruby, or JavaScript, while Behave binds them to Python. Choose the one matching your application stack so step definitions reuse existing test utilities.

Yes. Behave returns a non-zero exit code when any scenario fails, so Jenkins, GitLab CI, or GitHub Actions can fail the build automatically. Publish the Allure results folder as a build artifact for reporting.

Yes. AI assistants draft Given-When-Then scenarios from user stories or an OpenAPI specification. Review the output for missing negative cases and business rules, because generated scenarios often cover only the happy path.

AI tools detect duplicate or near-duplicate scenarios, suggest reusable parameterized steps, and cluster flaky failures by probable cause. This reduces the manual audit effort as feature files multiply across teams.

Fetch the token once in the environment.py before_all hook, store it on the context object, and attach it as an Authorization header in each request step. Keep credentials in environment variables, never in feature files.

Summarize this post with: