DEV Community

Vinícius Estevam
Vinícius Estevam

Posted on • Updated on

BDD Testing in .NET8

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:

Image description

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

Enter fullscreen mode Exit fullscreen mode

Image description


Then create two folders within this project named as Features and Steps.

Image description


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 Outline: Create a new member
    When I send a POST request to /Member with the following member details: "<Name>", "<Email>", "<Identifier>", "<BrazilianDocument>", "<BrazilianZipCode>", "<Phone>"
    Then the API response should be: "<StatusCode>"
Examples: 
    | Name       | Email                  | Identifier    | BrazilianDocument   | BrazilianZipCode    | Phone         | StatusCode   |
    | ""         | john.doe@example.com   | 0000bsi0000   | 12345678900         | 12345678            | 27900000000   | 400          |
    | John Doe   | john.doe@example.com   | 0000bsi0000   | 12345678900         | 12345678            | 27900000000   | 200          |

Scenario Outline: Update a new member
    When I send a PUT request to /Member/"<MemberId>" with the following member details: "<Name>", "<Email>", "<Identifier>", "<BrazilianDocument>", "<BrazilianZipCode>", "<Phone>"
    Then the API response should be: "<StatusCode>"
Examples: 
    | MemberId                             | Name             | Email                 | Identifier  | BrazilianDocument | BrazilianZipCode | Phone       | StatusCode |
    | 8db060d9-ca6f-4320-a972-c687d75accce | ""               | john.doe@example.com  | 0000bsi0000 | 12345678900       | 12345678         | 27900000000 | 400        |
    | 8db060d9-ca6f-4320-a972-c687d75accce | John Doe Updated | john.doe2@example.com | 1111bsi1111 | 12345678911       | 12345679         | 27912341234 | 200        |

Scenario Outline: Retrieve an existing member
    When I send a GET request to /Member/"<MemberId>"
    Then the API response should be: "<StatusCode>"
Examples: 
    | MemberId                             | StatusCode |
    | 28368835-8f04-4357-81b5-31d666314020 | 200        |

Scenario Outline: Delete an existing member
    When I send a DELETE request to /Member/"<MemberId>"
    Then the API response should be: "<StatusCode>"
Examples: 
    | MemberId                             | StatusCode |
    | 52ee8b49-9758-499a-b35e-93b61e10827a | 200        |
Enter fullscreen mode Exit fullscreen mode

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 const string BASE_URL = "https://localhost:3000/api/Member/";
        private readonly HttpClient _client;
        private HttpResponseMessage _response;
        private ApiDataProvider _provider;

        public MemberSteps()
        {
            _client = new HttpClient();
            _provider = new ApiDataProvider();
        }

        #region Check if API is running
        [Given("I have access to the member API")]
        public async Task IHaveAccessAPI()
        {
            var response = await _client.GetAsync(BASE_URL);
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }
        #endregion

        #region Post Request
        [When(@"I send a POST request to /Member with the following member details: ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)""")]
        public async Task WhenISendAPostRequest(string name, string email, string identifier, string document, string zipCode, string phone)
        {
            var member = new MemberRequestDTO();

            member.Name = name;
            member.Email = email;
            member.Identifier = identifier;
            member.BrazilianDocument = document;
            member.BrazilianZipCode = zipCode;
            member.Phone = phone;

            var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json");
            var response = await _client.PostAsync(BASE_URL, content);
            _response = response;
        }
        #endregion

        #region Put Request
        [When(@"I send a PUT request to /Member/""(.+)"" with the following member details: ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)""")]
        public async Task WhenISendAPutRequest(string memberId, string name, string email, string identifier, string document, string zipCode, string phone)
        {
            MemberResponseDTO member = await _provider.GetEntityById<MemberResponseDTO>("Member", memberId);

            member.Name = name;
            member.Email = email;
            member.Identifier = identifier;
            member.BrazilianDocument = document;
            member.BrazilianZipCode = zipCode;
            member.Phone = phone;

            var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json");
            var response = await _client.PutAsync(BASE_URL + memberId, content);
            _response = response;
        }
        #endregion

        #region Retrieve Request
        [When(@"I send a GET request to /Member/""(.+)""")]
        public async Task WhenISendAGetRequest(string memberId)
        {
            var response = await _client.GetAsync(BASE_URL + memberId);
            _response = response;
        }
        #endregion

        #region Delete Request
        [When(@"I send a DELETE request to /Member/""(.+)""")]
        public async Task WhenISendADeleteRequest(string memberId)
        {
            var response = await _client.DeleteAsync(BASE_URL + memberId);
            _response = response;
        }
        #endregion

        #region Check API Response
        [Then(@"the API response should be: ""(.+)""")]
        public async Task ThenApiResponse(string statusCode)
        {
            Assert.Equal(Convert.ToInt32(statusCode), (int)_response.StatusCode);
        }
        #endregion
    }

}
Enter fullscreen mode Exit fullscreen mode

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
{
Enter fullscreen mode Exit fullscreen mode

Fields and Constructor
The class has fields to hold an HttpClient instance, a base URL for the API, an ApiDataProvider instance and a response instance to store api response to each request:

private const string BASE_URL = "https://localhost:3000/api/Member/";
        private readonly HttpClient _client;
        private HttpResponseMessage _response;
        private ApiDataProvider _provider;

        public MemberSteps()
        {
            _client = new HttpClient();
            _provider = new ApiDataProvider();
        }
Enter fullscreen mode Exit fullscreen mode

Verifying API Availability
This step ensures the API is available:

[Given("I have access to the member API")]
        public async Task IHaveAccessAPI()
        {
            var response = await _client.GetAsync(BASE_URL);
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }

Enter fullscreen mode Exit fullscreen mode

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(string statusCode)
        {
            Assert.Equal(Convert.ToInt32(statusCode), (int)_response.StatusCode);
        }

Enter fullscreen mode Exit fullscreen mode

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 /Member with the following member details: ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)""")]
        public async Task WhenISendAPostRequest(string name, string email, string identifier, string document, string zipCode, string phone)
        {
            var member = new MemberRequestDTO();

            member.Name = name;
            member.Email = email;
            member.Identifier = identifier;
            member.BrazilianDocument = document;
            member.BrazilianZipCode = zipCode;
            member.Phone = phone;

            var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json");
            var response = await _client.PostAsync(BASE_URL, content);
            _response = response;
        }

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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/""(.+)"" with the following member details: ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)"", ""(.+)""")]
        public async Task WhenISendAPutRequest(string memberId, string name, string email, string identifier, string document, string zipCode, string phone)
        {
            MemberResponseDTO member = await _provider.GetEntityById<MemberResponseDTO>("Member", memberId);

            member.Name = name;
            member.Email = email;
            member.Identifier = identifier;
            member.BrazilianDocument = document;
            member.BrazilianZipCode = zipCode;
            member.Phone = phone;

            var content = new StringContent(JsonSerializer.Serialize(member), Encoding.UTF8, "application/json");
            var response = await _client.PutAsync(BASE_URL + memberId, content);
            _response = response;
        }
Enter fullscreen mode Exit fullscreen mode

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/""(.+)""")]
        public async Task WhenISendADeleteRequest(string memberId)
        {
            var response = await _client.DeleteAsync(BASE_URL + memberId);
            _response = response;
        }
Enter fullscreen mode Exit fullscreen mode

Run Test

You can run the test in Visual Studio IDE or cmd.

Image description

dotnet test
Enter fullscreen mode Exit fullscreen mode

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:

Image description


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.

Top comments (0)