Getting Started with BDD Testing in .NET Using Reqnroll, xUnit, and Testcontainers 🧪


Why BDD Testing?

Behavior-Driven Development (BDD) is a powerful approach to software testing that bridges the gap between technical and non-technical team members. By defining test cases in a human-readable format, BDD ensures that requirements are well understood and validated early in the development cycle.

Benefits of BDD Testing

  • Improves Collaboration: Encourages communication between developers, testers, and business stakeholders.
  • Readable Test Cases: Uses plain English syntax (Gherkin) to define test cases, making them easy to understand.
  • Living Documentation: BDD test scenarios act as up-to-date documentation for system behavior.
  • Early Bug Detection: Helps identify issues at the specification stage, reducing costly fixes later.

In this guide, we’ll explore how to set up BDD testing in a .NET environment using Reqnroll, xUnit, and Testcontainers.


Getting Started with Reqnroll, xUnit, and Testcontainers

Prerequisites

Make sure you have the following installed:

  • .NET SDK (>= 6.0)
  • Docker (for Testcontainers)

Setting Up a .NET Test Project

Create a new test project:

dotnet new xunit -n BddTests
cd BddTests

Install dependencies:

dotnet add package Reqnroll.xUnit

dotnet add package Testcontainers.CosmosDb

Writing a BDD Test with Reqnroll

Reqnroll uses Gherkin syntax to define test scenarios. Create a new folder Features and add a file Login.feature:

Feature: User Authentication
  As a user
  I want to log into my account
  So that I can access my dashboard

  Scenario: Successful Login
    Given a user exists with username "testuser" and password "P@ssword123"
    When the user logs in with username "testuser" and password "P@ssword123"
    Then the login should be successful

Implementing Step Definitions

Create a step definitions file LoginSteps.cs inside a StepDefinitions folder:

using Reqnroll;
using Xunit;

public class LoginSteps
{
    private string _username;
    private string _password;
    private bool _loginSuccess;

    [Given("a user exists with username {string} and password {string})]
    public void GivenAUserExists(string username, string password)
    {
        _username = username;
        _password = password;
    }

    [When("the user logs in with username {string} and password {string})]
    public void WhenUserLogsIn(string username, string password)
    {
        _loginSuccess = username == _username && password == _password;
    }

    [Then("the login should be successful")]
    public void ThenLoginShouldBeSuccessful()
    {
        Assert.True(_loginSuccess);
    }
}

Running the Tests

Run the tests with:

dotnet test

Integrating Testcontainers

To make our tests more realistic, let’s run a CosmosDB emulator using Testcontainers.

Setting Up a Test Database

Modify LoginSteps.cs to use a CosmosDB container:

using Testcontainers.CosmosDb;
using Microsoft.Azure.Cosmos;
using System.Threading.Tasks;

public class DatabaseFixture : IAsyncLifetime
{
    public CosmosDbContainer DbContainer { get; } = new CosmosDbBuilder()
        .WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator")
        .Build();

    public CosmosClient CosmosClient { get; private set; }
    public Database Database { get; private set; }
    public Container Container { get; private set; }

    public async Task InitializeAsync()
    {
        await DbContainer.StartAsync();
        CosmosClient = new CosmosClient(DbContainer.GetConnectionString());
        Database = await CosmosClient.CreateDatabaseIfNotExistsAsync("TestDatabase");
        Container = await Database.CreateContainerIfNotExistsAsync("Users", "/id");
    }

    public async Task DisposeAsync()
    {
        await DbContainer.DisposeAsync();
    }
}

public class LoginSteps : IClassFixture<DatabaseFixture>
{
    private readonly DatabaseFixture _dbFixture;
    private bool _loginSuccess;

    public LoginSteps(DatabaseFixture dbFixture)
    {
        _dbFixture = dbFixture;
    }

    [Given("a user exists with username {string} and password {string}]
    public async Task GivenAUserExists(string username, string password)
    {
        await _dbFixture.Container.UpsertItemAsync(new { id = username, username, password });
    }

    [When("the user logs in with username {string} and password {string})]
    public async Task WhenUserLogsIn(string username, string password)
    {
        var query = _dbFixture.Container.GetItemLinqQueryable<dynamic>(true)
            .Where(u => u.username == username && u.password == password)
            .ToFeedIterator();
        _loginSuccess = query.HasMoreResults;
    }

    [Then("the login should be successful")]
    public void ThenLoginShouldBeSuccessful()
    {
        Assert.True(_loginSuccess);
    }
}

Running the Tests with Testcontainers

Make sure Docker is running and execute:

dotnet test

Your test will spin up a real CosmosDB emulator, insert test data, and validate login functionality!


Conclusion

Using Reqnroll with xUnit and Testcontainers, we’ve successfully implemented BDD testing in a .NET environment. This approach ensures:

  • Readable and maintainable test scenarios
  • Integration with real services using Docker
  • Early validation of system behavior

BDD isn't just about writing tests—it's about improving collaboration and making your codebase more predictable. Try integrating it into your projects and see the benefits firsthand!


What are your thoughts on BDD testing? Have you used Reqnroll before? Share your experience in the comments!