Blog

How to Implement Behavior-Driven Development Using Specflow in .NET

Behavior-driven development (BDD) is an Agile software methodology that can bridge the gap between business-focused and technical people by ensuring that software is built around the behavior a user expects to experience when using it. BDD combines and refines test-driven development (TDD) and acceptance testing to help reduce excessive code, eliminate unnecessary features, and improve alignment with desired business outcomes.

In this blog, we’ll explore creating a BDD test suite using Specflow, a testing framework that supports BDD. With Specflow, you can translate plain-English Gherkin scenarios to a .NET test project using three worlds: Development, Testing, and Product. Specflow is available as a nuget package that can be added to your test projects; there is also a CLI version that provides features to integrate into a CI/CD pipeline. 

Getting ready for your project

  • If needed, install Visual Studio Professional 2022, which we’ll be using for this project.
  • Install the Specflow extension in Visual Studio Professional 2022 from the Visual Studio marketplace. This extension allows you to:
    • Find Specflow project templates from the  “Add new project” window.
    • Use Feature file utilities, a set of functions that support BDD, like generate steps definitions and feature file markup to easily read scenarios and identify issues.

1.png

2.png

  • Create your system under Test Project: We’ll use WebAPI .NET with a controller to receive employee personal information requests. It will compare against previous information stored in an in-memory database.
  • Create a Specflow Test Project: With the Specflow extension you installed,  you can create a BDD Specflow project using a template. Right-click your solution and select the Add -> New Project… option from the Context menu.

3.png

  • In the Test Category, find “SpecFlow project” and click Next. 
  • Select the Test Framework; for our project, we’ll use xUnit and the Add FluentAssertions library option to get the pre-Installed the FluentAssertions nuget.

4.png

That’s it–you’ve added a BDD test suite project to your solution. 

5.png

Formulate scenarios

In our project, we’ll implement a feature, called Employee Delta Detection, to track changes to employee information in our system. 

You’ll see this represented in the following three scenarios. You’ll also notice that these scenarios use the Background and Scenario Outline tools, which we’ll examine in more detail in the second article of this series: How to write Gherkins compatible with BDD

Background: 
  Given the MyService is running
  And receives a employee status request
    | property        | value      |
    | EmployeeId      | 12345      |
    | FirstName       | First      |
    | LastName        | Last       |
    | MiddleName      | Middle     |
    | Phone           | 88888888   |
    | SSN             | 1234566    |
    | Address         | My address |
    | ZipCode         | 11-3565    |
    | EndContractDate | 01/01/2023 |
    | Dob             | 01/01/1980 |
    | Status          | VALID      |


Scenario: Employee Status Request. Bad Request
And the employee status request Id is invalid
When MyService runs the delta detector
Then the response code should be 400

Scenario: Employee Status Request. Delta Detected
And the previous employee entry has the  value 
And the current employee has the  value 
When MyService runs the delta detector
Then the response result should be activity
And the response reason should be 
And the response code should be 200
And a new audit entry was generated

Examples: 
| previousProperty | previousValue | currentProperty | currentValue | reason                           |
| Phone            | 88446655      | Phone           | 88446688     | PHONE_DELTA_DETECTED             |
| LastName         | Last          | LastName        | Last2        | LAST_NAME_DELTA_DETECTED         |
| FirstName        | First         | FirstName       | First2       | FIRST_NAME_DELTA_DETECTED        |
| MiddleName       | Middle        | MiddleName      | Middle2      | MIDDLE_NAME_DELTA_DETECTED       |
| SSN              | 123456        | SSN             | 123455       | SSN_DELTA_DETECTED               |
| ZipCode          | 123-569       | ZipCode         | 123-566      | ZIP_CODE_DELTA_DETECTED          |
| Address          | Address 1     | Address         | Address  2   | ADDRESS_DELTA_DETECTED           |
| EndContractDate  | 01/01/2022    | EndContractDate | 01/01/2023   | END_CONTRACT_DATE_DELTA_DETECTED |
| Dob              | 01/01/1980    | Dob             | 01/01/1982   | DOB_DELTA_DETECTED               |
| Status           | EXPIRED       | Status          | VALID        | STATUS_DELTA_DETECTED            |
| Status           | VALID         | Status          | SUSPENDED    | STATUS_DELTA_DETECTED            |

Scenario: Employee Status Request. No Changes
And the previous employee entry is equivalent to the current
When MyService runs the delta detector
Then the response result should be no activity
And the response code should be 200
And the audit entry was generated

The first scenario validates the Bad Request scenario in case of an invalid employee identification.

The second scenario provides all the scenarios for which a change should be detected.

You can read each of these examples as following:

Scenario: Employee Status Request. Delta Detected
And the previous employee entry has the Status value VALID
And the current employee has the Status value SUSPENDED
When MyService runs the delta detector
Then the response result should be activity
And the response reason should be STATUS_DELTA_DETECTED
And the response code should be 200
And a new audit entry was generated

The third scenario handles instances for which the previous and current employee status is the same.

Now let’s build the test suite to see how this translates into a feature file and steps definitions.

Build the test suite

Add a feature file

  • Add a new feature file into the features folder of your BDD Test Suite project. There will be a SpecFlow section in the Add New Item Window:

6.png

  • Add a short description of your feature and paste your scenarios into the feature file:

7.png

Define steps

As you can see, scenario steps are highlighted in purple. Scenario steps highlighted in purple indicate that the step isn’t tied to a step definition method. 

  • Right-click into the feature file content and select Define Steps.

8.png

The Define Steps window will display all the steps definitions that will be created.

  • Edit the class name if required and click the Create button.

9.png

Now a new class is created under the StepsDefinition folder of your BDD test suite with all the step definitions for each step in the feature file. 

By default the steps are created using regular expressions match, so the step will be tied to the corresponding method using the regular expression in the method attribute [Given, When Then], like this:

9.1.png

On some occasions, the wizard doesn’t detect string parameters. If you return to the feature file, you’ll find a few untied steps shown in purple.

11.png

  • Let’s adjust some of the steps to help identify the parameters by replacing the value string in the attributes with a matching regex, like this:
//Before
[Given(@"the previous employee entry has the Phone (.*)")]
public void GivenThePreviousEmployeeEntryHasThePhone(int p0)
{
     throw new PendingStepException();
}

//After
[Given(@"the previous employee entry has the (.*) value (.*)")]
public void GivenThePreviousEmployeeEntryHasThePhone(string property, string value)
{
     throw new PendingStepException();
}
  • Repeat these steps for any other step definitions that should receive a string parameter.

Use hooks

As you can see in the ‘GivenReceivesAEmployeeStatusRequest’ method above, a table is received as a parameter. In our example, we can access any data present in the feature file by rows and columns. But iterating through two different arrays isn’t typically the best approach, so let’s use a hook to transform the table into a clean model.

  • Create a new folder called Hooks, add a new C# class, and add a hook like the following:
[StepArgumentTransformation(@"receives an employee status request")]
public EmployeeStatus ItemModelTransformation(Table table)
{
	return table.CreateInstance();
}

The attribute StepArgumentTransformation indicates that this hook should be used to transform the table in the step “receives an employee status request.” 

  • Use the CreateInstance method to transform the table into an object instance. It’s important that the properties in the model class match with the values in the Feature File table.

10.png

  • Go back to the Step Definition class and update the corresponding method to receive the new model:
[Given(@"receives a employee status request")]
public void GivenReceivesAEmployeeStatusRequest(EmployeeStatus employeeStatus)
{
      throw new PendingStepException();
}

Build the support

Before we can execute, we need to build models, entities, and controller shells:

  • Identify external dependencies that your implementation could have. In this case, the database is our unique dependency, which will be handled with an in-memory approach.
  • Define which data should be shared between steps.
  • Complete dependency injection registration.

 

DataDriver

Create a class under the Support folder to add any additional interaction related to data. Here, we’ll create two methods:

  1. Insert an employee status to preconfigure a previous entry for the Delta Detection scenarios.
  2. Get audit entry to validate that the audit entry has been created.

If your implementation has other external dependencies that require Mocks, this class is a good place to add them and then configure your setups and fake data.

   [Binding]
    public class DataDriver
    {
        private readonly IMapper _mapper;
        private readonly IDbContextFactory _contextFactory;

        public DataDriver(IMapper mapper, IDbContextFactory contextFactory)
        {
            _mapper = mapper;
            _contextFactory = contextFactory;
        }

        public void InsertEmployeeStatus(EmployeeStatus employeeStatus)
        {
            using (var c = _contextFactory.CreateDbContext())
            {
                var employeeEntity = _mapper.Map(employeeStatus);
                c.Add(employeeEntity);
                c.SaveChanges();
            }
        }

        public EmployeeStatusAudit GetAuditEntry(int auditEntryId)
        {
            using (var c = _contextFactory.CreateDbContext())
            {
                var auditEntity = c.EmployeeStatusAudit.FirstOrDefault(x => x.RowId == auditEntryId);
                return _mapper.Map(auditEntity);
            }
        }
    }

Context

Data sharing between steps is required. Precondition steps should be able to store data in a place that can be available for succeeding steps. This is also true if you need to store test results that will be put under validation with assertion. 

In this project, we’ll store the input request model in the response result. 

  • Create a new C# class under the Support folder:
[Binding]
public class DeltaDetectionContext
{
	public EmployeeStatus InputModel { get; set; }
	public ActionResult Response { get; set; }
}

Scenario dependencies

You might have noticed that we’ve been using the binding attribute in all the C# classes. This allows the Specflow test suite to inject any class with the binding attribute into another one. 

  • We’ll use the Microsoft dependency injection provider through the solidToken nuget, so if you need to, install it now.Binding classes will be automatically added.
  • Register any dependencies required for your implementation (once completed).

In this case, we’ll add all the dependencies required by the controller and configure an in-memory database to integrate database validations:

namespace BDDSuite.Support
{
	public static class ScenarioDependencies
	{
		[ScenarioDependencies]
		public static IServiceCollection CreateServices()
		{
			var services = new ServiceCollection();
			services.AddAutoMapper(typeof(EmployeeStatusProfile));
			services.AddScoped<IDeltaDetector, DeltaDetector>();
			services.AddScoped<IEmployeeHistory, EmployeeHistory>();
			services.AddDbContextFactory(b =>
						   b.UseInMemoryDatabase("employeeStatus")
						);
			services.AddScoped<ILogger, NullLogger>();
			services.AddTransient();
			return services;
		}
	}
}

Driver

Now we connect the step definitions with the actual controller that will have the implementation. 

  • Create a new class under the Driver folder.
  • Inject the context, DataDriver, the system under test (the controller), and a mapper instance to easily map data through models.

12.png

Arrange

Now we need code to fulfill all the given steps:

  • Initialize the system under test if required.
  • Setup input model.
  • Setup given preconditions.

Review each method comment to identify which method corresponds to which scenario step:

#region "Arrange"

///
/// Given the MyService is running
/// public void InitializeController()
{
	//Use this method to initialize the system under test in case it is not possible to inject in driver controller
}

////// And receives a employee status request
/// public void SetUpInputModel(EmployeeStatus employeeStatus)
{
	_context.InputModel = employeeStatus;
}

////// And the employee status request Id is invalid
/// public void SetUpInvalidId()
{
	_context.InputModel.EmployeeId = string.Empty;
}

////// And the previous employee entry is equivalent to the current
/// public void SetPrevious()
{
	_dataDriver.InsertEmployeeStatus(_context.InputModel);
}

////// And the previous employee entry has the  
/// public void SetPreviousValue(string property, string value)
{
	var previous = _mapper.Map(_context.InputModel);
	previous.GetType().GetProperty(property).SetValue(previous, value);
	_dataDriver.InsertEmployeeStatus(previous);
}

////// And the current employee entry has the  
/// public void SetCurrentValue(string property, string value)
{
	_context.InputModel.GetType().GetProperty(property).SetValue(_context.InputModel, value);
}
#endregion

Act

Next, we call the feature implementation:

  • Add a new method to the driver to invoke the controller under test.
  • Save in the context the result to be validated in the following steps:
#region "Act"
///
/// When MyService runs the delta detector
/// public void InvokeController()
{
	_context.Response = _sut.Post(_context.InputModel);
}
#endregion

Assert

Next we validate every condition in the Then scenario steps:

  • Using fluent assertion, validate httpCodes and response values:
#region "Assert"
///
/// Then the response code should be ###
/// public void ValidateResponseCode(int httpCode)
{
	if(httpCode == 400)
	{
		_context.Response.Should().BeOfType()
		.Which.StatusCode.Should().Be(httpCode);
	}
	else 
	{
		_context.Response.Should().BeOfType()
		.Which.StatusCode.Should().Be(httpCode);
	}	
}

////// And the audit entry is not generated
/// public void VerifyAuditEntryNotGenerated()
{
	((OkObjectResult)_context.Response).Value.Should().BeOfType()
		.Which.AuditId.Should().BeLessThanOrEqualTo(0);
}

///// And a new audit entry was generated
/// public void VerifyAuditEntryGenerated()
{
	((OkObjectResult)_context.Response).Value.Should().BeOfType()
		.Which.AuditId.Should().BeGreaterThan(0);
	var okResult = _context.Response as OkObjectResult;

	var response = okResult?.Value as EmployeeStatusResponse;
	var audit = _dataDriver.GetAuditEntry(response.AuditId);
	audit.Should().NotBeNull();
}

////// Then the response result should be 
/// public void ValidateResponseResult(string result)
{
	((OkObjectResult)_context.Response).Value.Should().BeOfType()
		.Which.Result.Should().Be(result);
}

////// Then the response reason should be 
/// public void ValidateResponseReason(string reason)
{
	((OkObjectResult)_context.Response).Value.Should().BeOfType()
		.Which.Reason.Should().Be(reason);
}
#endregion
  • Inject into the step definitions class in the driver and call each method depending on the step to execute:
[Binding]
    public class EmployeeStatusDeltaDetectionStepDefinitions
    {
        private readonly EmployeeStatusDeltaDetectionDriver _driver;
        public EmployeeStatusDeltaDetectionStepDefinitions(EmployeeStatusDeltaDetectionDriver driver)
        {
            _driver = driver;
        }

        [Given(@"the MyService is running")]
        public void GivenTheMyServiceIsRunning()
        {
            _driver.InitializeController();
        }

        [Given(@"receives a employee status request")]
        public void GivenReceivesAEmployeeStatusRequest(EmployeeStatus employeeStatus)
        {
            _driver.SetUpInputModel(employeeStatus);
        }

        [Given(@"the employee status request Id is invalid")]
        public void GivenTheEmployeeStatusRequestIdIsInvalid()
        {
            _driver.SetUpInvalidId();
        }

        [When(@"MyService runs the delta detector")]
        public void WhenMyServiceRunsTheDeltaDetector()
        {
            _driver.InvokeController();
        }

        [Then(@"the response code should be (.*)")]
        public void ThenTheResponseCodeShouldBe(int httpCode)
        {
            _driver.ValidateResponseCode(httpCode);
        }

        [Then(@"the audit entry is not generated")]
        public void ThenTheAuditEntryIsNotGenerated()
        {
            _driver.VerifyAuditEntryNotGenerated();
        }

        [Given(@"the previous employee entry has the (.*) value (.*)")]
        public void GivenThePreviousEmployeeEntryHas(string property, string value)
        {
            _driver.SetPreviousValue(property, value);
        }

        [Given(@"the current employee has the (.*) value (.*)")]
        public void GivenTheCurrentEmployeeHasThe(string property, string value)
        {
            _driver.SetCurrentValue(property, value);
        }

        [Then(@"the response result should be (.*)")]
        public void ThenTheResponseResultShouldBeActivity(string result)
        {
            result = result == "activity" ? "ACTIVITY" : "NO_ACTIVITY";
            _driver.ValidateResponseResult(result);
        }

        [Then(@"the response reason should be (.*)")]
        public void ThenTheResponseReasonShouldBe(string reason)
        {
            _driver.ValidateResponseReason(reason);
        }

        [Then(@"a new audit entry was generated")]
        public void ThenANewAuditEntryWasGenerated()
        {
            _driver.VerifyAuditEntryGenerated();
        }

        [Given(@"the previous employee entry is equivalent to the current")]
        public void GivenThePreviousEmployeeEntryIsEquivalentToTheCurrent()
        {
            _driver.SetPrevious();
        }
    }

Run tests

Compile your solution and go to the Test Explorer. You should see one test per each scenario in the feature file:

14.png

When you run the test, all scenarios should pass:

15.png

Coming up next

In the next article in this series, we’ll explore how to write Gherkins that are compatible with BDD. 

Sources

Specflow

Github

Nuget

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM