Introduction
In this tutorial you will understand what is Behavior-Driven Development (BDD) and your benefits.
Behavior-Driven Development (BDD)
Behavior-Driven Development (BDD) is an agile methodology that enhances collaboration among all project participants, regardless of technical knowledge. It builds on Test-Driven Development (TDD) by using natural language to describe test scenarios, making them easy to understand for everyone. BDD aims to improve communication, reduce misunderstandings, and ensure the software meets real user needs.
Benefits Of BDD
- Clear overview of the project.
- Improves understanding of the parties involved.
- Allows automation of tests and documents.
- Contributes to efficient development.
How BDD Works ?
Scenario definition (features files):
The team must define through natural language how the system should behave in each scenario, which can be understood as the CRUD operation for a system entity. Cases of success and error must be taken into account, as well as what the system should return to the user in each action.
BDD syntax uses 3 keywords:
Given: Describes the initial context of the scenario.
When: Describes the action that triggers the scenario.
Then: Describes the expected result of the action.
Scenario automation (steps files):
After defining the scenarios, tests can be automated based on feature documents, these tests serve to validate the scenarios created and are run continuously throughout the project’s development, to identify bugs quickly.
Tools
- C#
- .NET8
- Visual Studio 2022
Configuration
To implement the BDD test in .NET8 we will use the Xunit.Gherkin.Quick library available on link
Within your api’s solution, right-click and select the Add new Project option and select the xUnit Test Project type project. At the end you should see a project in this structure:
To install Xunit.Gherkin.Quick you can run this commands in cmd or add packages in Nuget Mangement via Visual Studio.
dotnet add package Xunit.Gherkin.Quick
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Newtonsoft.Json
Then create two folders within this project named as Features and Steps.
Test Implementation
Let’s start making the feature file for the member entity, create a file called MemberFeatures.feature inside the feature folder, then let’s set the scene, in this tutorial we will cover an error and a success case for registration and editing.
Feature: Member CRUD Background: Given I have access to the member API Scenario: Create a new member When I send a POST request to '/Members' with the following member details: | Name | Email | Identifier | BrazilianDocument | BrazilianZipCode | Phone | |----------|----------------------|-------------|-------------------|-------------------|-------------| | | [email protected] | 0000bsi0000 | 12345678900 | 12345678 | 27900000000 | | John Doe | [email protected] | 0000bsi0000 | 12345678900 | 12345678 | 27900000000 | Then the API response should be: | Status | Code | |---------|-------| | Error | 400 | | Success | 200 | Scenario: Retrieve an existing member Given I have a member with ID "28368835-8f04-4357-81b5-31d666314020" in database When I send a GET request to '/Member/28368835-8f04-4357-81b5-31d666314020' Then the API response should be: | Status | Code | |---------|-------| | Success | 200 | Scenario: Update an existing member Given I have a member with ID "8db060d9-ca6f-4320-a972-c687d75accce" in database When I send a PUT request to '/Member/8db060d9-ca6f-4320-a972-c687d75accce' with data: | Name | Email | Identifier | BrazilianDocument | BrazilianZipCode | Phone | |----------|----------------------|-------------|-------------------|-------------------|-------------| | John Doe | | 0000bsi0000 | 12345678900 | 12345678 | 27900000000 | | John Doe | [email protected] | 0000bsi0000 | 12345678900 | 12345678 | 27900000000 | Then the API response should be: | Status | Code | |---------|-------| | Error | 400 | | Success | 200 | Scenario: Delete an existing member Given I have a member with ID "52ee8b49-9758-499a-b35e-93b61e10827a" in database When I send a DELETE request to '/Member/52ee8b49-9758-499a-b35e-93b61e10827a' Then the API response should be: | Status | Code | |---------|-------| | Success | 200 |
Now let’s implement the steps test based on the resource file we just created in the previous step, to create a file called MemberSteps.cs inside the Step folder.
using Gherkin.Ast;
using Application.DTOs.Request;
using Application.DTOs.Response;
using Test.Shared;
using System.Net;
using System.Text;
using System.Text.Json;
using Xunit.Gherkin.Quick; namespace SlaveOneBack.Test.Steps
{ [FeatureFile("../../../Features/MemberFeature.feature")] public class MemberSteps : Xunit.Gherkin.Quick.Feature { private readonly HttpClient _client; private const string BASE_URL = "https://localhost:3000/Member/"; private ApiDataProvider _provider; private List<int> listOfStatusCode = new List<int>(); private string MemberID = ""; public MemberSteps() { _client = new HttpClient(); _provider = new ApiDataProvider(); } #region Verifica dispinibilidade da API [Given("I have access to the member API")] public async Task IHaveAccessMemberAPI() { var response = await _client.GetAsync(BASE_URL); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } #endregion #region Data entry by "Step Attributes" [Given(@"I have a member with ID ""(.*)"" in database")] public void GetMemberId(string Id) { MemberID = Id; } #endregion #region Verificação dos códigos da API [Then("the API response should be:")] public async Task ThenApiResponse(DataTable dataTable) { List<int> listOfExpectedResponse = new List<int>(); foreach (var row in dataTable.Rows.Skip(2)) { listOfExpectedResponse.Add(int.Parse(row.Cells.ElementAt(1).Value)); } for (int i = 0; i < listOfExpectedResponse.Count - 1; i++) { Assert.Equal(listOfExpectedResponse.ElementAt(i), listOfStatusCode.ElementAt(i)); } } #endregion #region Create a new member [When("I send a POST request to '/Members' with the following member details:")] public async Task PostRequest(DataTable dataTable) { listOfStatusCode.Clear(); var member = new MemberRequestDTO(); foreach (var row in dataTable.Rows.Skip(2)) { member.Name = row.Cells.ElementAt(0).Value; member.Email = row.Cells.ElementAt(1).Value; member.Identifier = row.Cells.ElementAt(2).Value; member.BrazilianDocument = row.Cells.ElementAt(3).Value; member.BrazilianZipCode = row.Cells.ElementAt(4).Value; member.Phone = row.Cells.ElementAt(5).Value; var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json"); var response = await _client.PostAsync(BASE_URL, content); listOfStatusCode.Add((int)response.StatusCode); } } #endregion #region Retrieve an existing member [When("I send a GET request to '/Member/28368835-8f04-4357-81b5-31d666314020'")] public async Task GetByIdRequest() { listOfStatusCode.Clear(); var response = await _client.GetAsync(BASE_URL + MemberID); listOfStatusCode.Add((int)response.StatusCode); } #endregion #region Update an existing member [When("I send a PUT request to '/Member/8db060d9-ca6f-4320-a972-c687d75accce' with data:")] public async Task PutRequest(DataTable dataTable) { listOfStatusCode.Clear(); MemberResponseDTO member = await _provider.GetEntityById<MemberResponseDTO>("Member", MemberID); foreach (var row in dataTable.Rows.Skip(2)) { member.Name = row.Cells.ElementAt(0).Value; member.Email = row.Cells.ElementAt(1).Value; member.Identifier = row.Cells.ElementAt(2).Value; member.BrazilianDocument = row.Cells.ElementAt(3).Value; member.BrazilianZipCode = row.Cells.ElementAt(4).Value; member.Phone = row.Cells.ElementAt(5).Value; var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json"); var response = await _client.PutAsync(BASE_URL + MemberID, content); listOfStatusCode.Add((int)response.StatusCode); } } #endregion #region Delete member [When("I send a DELETE request to '/Member/52ee8b49-9758-499a-b35e-93b61e10827a'")] public async Task DeleteRequest() { listOfStatusCode.Clear(); var response = await _client.DeleteAsync(BASE_URL + MemberID); listOfStatusCode.Add((int)response.StatusCode); } #endregion }
}
So how this file works ?
Feature File Association
The MemberSteps class is associated with the MemberFeature.feature file using the [FeatureFile] attribute:
[FeatureFile("../../../Features/MemberFeature.feature")]
public class MemberSteps : Xunit.Gherkin.Quick.Feature
{
Fields and Constructor
The class has fields to hold an HttpClient instance, a base URL for the API, an ApiDataProvider instance, a list of status codes, and a member ID:
private readonly HttpClient _client;
private const string BASE_URL = "https://localhost:3000/api/Member/";
private ApiDataProvider _provider;
private List<int> listOfStatusCode = new List<int>();
private string MemberID = ""; public MemberSteps()
{ _client = new HttpClient(); _provider = new ApiDataProvider();
}
Step Definitions
Verifying API Availability
This step ensures the API is available:
[Given("I have access to the member API")]
public async Task IHaveAccessMemberAPI()
{ var response = await _client.GetAsync(BASE_URL); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Setting Member ID
This step sets the member ID from the given parameter:
[Given(@"I have a member with ID ""(.*)"" in database")]
public void GetMemberId(string Id)
{ MemberID = Id;
}
Verifying API Response Codes
This step checks that the API response codes match expected values:
[Then("the API response should be:")]
public async Task ThenApiResponse(DataTable dataTable)
{ List<int> listOfExpectedResponse = new List<int>(); foreach (var row in dataTable.Rows.Skip(2)) { listOfExpectedResponse.Add(int.Parse(row.Cells.ElementAt(1).Value)); } for (int i = 0; i < listOfExpectedResponse.Count - 1; i++) { Assert.Equal(listOfExpectedResponse.ElementAt(i), listOfStatusCode.ElementAt(i)); }
}
Creating a New Member
This step sends a POST request to create a new member and records the response status codes:
[When("I send a POST request to '/Members' with the following member details:")]
public async Task PostRequest(DataTable dataTable)
{ listOfStatusCode.Clear(); var member = new MemberRequestDTO(); foreach (var row in dataTable.Rows.Skip(2)) { member.Name = row.Cells.ElementAt(0).Value; member.Email = row.Cells.ElementAt(1).Value; member.Identifier = row.Cells.ElementAt(2).Value; member.BrazilianDocument = row.Cells.ElementAt(3).Value; member.BrazilianZipCode = row.Cells.ElementAt(4).Value; member.Phone = row.Cells.ElementAt(5).Value; var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json"); var response = await _client.PostAsync(BASE_URL, content); listOfStatusCode.Add((int)response.StatusCode); }
}
Retrieving an Existing Member
This step sends a GET request to retrieve a member by ID and records the response status code:
[When("I send a GET request to '/Member/28368835-8f04-4357-81b5-31d666314020'")]
public async Task GetByIdRequest()
{ listOfStatusCode.Clear(); var response = await _client.GetAsync(BASE_URL + MemberID); listOfStatusCode.Add((int)response.StatusCode);
}
Updating an Existing Member
This step sends a PUT request to update a member’s details and records the response status codes:
[When("I send a PUT request to '/Member/8db060d9-ca6f-4320-a972-c687d75accce' with data:")]
public async Task PutRequest(DataTable dataTable)
{ listOfStatusCode.Clear(); MemberResponseDTO member = await _provider.GetEntityById<MemberResponseDTO>("Member", MemberID); foreach (var row in dataTable.Rows.Skip(2)) { member.Name = row.Cells.ElementAt(0).Value; member.Email = row.Cells.ElementAt(1).Value; member.Identifier = row.Cells.ElementAt(2).Value; member.BrazilianDocument = row.Cells.ElementAt(3).Value; member.BrazilianZipCode = row.Cells.ElementAt(4).Value; member.Phone = row.Cells.ElementAt(5).Value; var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json"); var response = await _client.PutAsync(BASE_URL + MemberID, content); listOfStatusCode.Add((int)response.StatusCode); }
}
Deleting a Member
This step sends a DELETE request to delete a member by ID and records the response status code:
[When("I send a DELETE request to '/Member/52ee8b49-9758-499a-b35e-93b61e10827a'")]
public async Task DeleteRequest()
{ listOfStatusCode.Clear(); var response = await _client.DeleteAsync(BASE_URL + MemberID); listOfStatusCode.Add((int)response.StatusCode);
}
Run Test
You can run the test in Visual Studio IDE or cmd.
dotnet test
I choose to run via IDE then i got this following report from our test, if you choose run by terminal you will receive the report on terminal:
Conclusion
Working with BDD (Behavior-Driven Development) at LEDS has been an has been an incredible experience, writing test scenarios right after requirements documentation helps reduce uncertainty and decrease the error rate during a development phase. Furthermore, test automation allows, with each new change or implementation of new functionalities, it is possible to validate these changes efficiently. This ensures greater security and confidence in continuous development.
Another significant benefit is that everyone involved in the project, regardless of their area of expertise, is fully aware of what to expect from each new version of the project. The BDD is read in natural language, which makes communication clear and accessible to everyone, making it easier to understand what is being developed and tested. This approach promotes more integrated and effective collaboration between teams, resulting in a higher quality final product.
Discover more from Coursity
Subscribe to get the latest posts sent to your email.