Rasmus Olsson

Behavior driven development with specflow

Tags: dotnet,
August 25, 2023

BDD is a technique that enables teams to grasp project requirements clearly through a shared language. This approach ensures that all project participants collaborate effectively on solutions. Utilizing the common language of Gherkin (Given, When, Then), it simplifies and clarifies the features of complex projects. BDD enhances communication among stakeholders, including developers, testers, and project owners, who need to work closely together, thereby minimizing the potential for misunderstandings.

In this post, we will have a brief look how to get started with Behavior driven development with SpecFlow.

SpecFlow is a tool that supports BDD for .NET projects. There are a other BDD tools on the market like LightBDD and NSpec but SpecFlow is considered the most popular.

SpecFlow comes with plugin support for both Visual Studio and Rider IDE.

We will start by install this through the plugin manager. I'll be using Rider IDE in this example.

Go to settings -> plugin -> search for SpecFlow and click install bdd-plugin-rider

Next we will create a Specflow template project.

Add new project -> search for Specflow and then create.

This will give us a .net6 csproj (currently) with some Specflow preinstalled packages:

  • TechTalk.SpecFlow - the official package for Specflow
  • SpecFlow.NUnit - allowing us to run SpecFlow on the NUnit test runner and enables you to run your BDD scenarios as NUnit tests.
  • SpecFlow.Plus.LivingDocPlugin - enables generating documentation out of the Specflow tests and features.

While the template defaults to using NUnit, it's worth noting that you can also select xUnit from the dropdown menu if you prefer. Other test runners, such as MSTest and the SpecFlow+ Runner, are available, but they might not be directly supported in the Rider IDE template.

In this example, we'll proceed with NUnit as our test runner. However, I plan to explore and compare the SpecFlow+ Runner with NUnit in a future post to highlight their differences and potential advantages.

Upon creating the SpecFlow project template, you'll notice the project structure typically includes several key directories:

  • Driver - This directory is often used to store the setup code and helpers that drive the tests, acting as a bridge between the step definitions and your application. It's where you'll write code to interact directly with your app, managing state, inputs, and outputs for your tests.
  • Features - Here, you'll find the .feature files written in Gherkin syntax. These files describe your application's features and scenarios in a language that's understandable by all stakeholders, serving as the basis for your SpecFlow tests.
  • Hooks - Hooks are special methods in SpecFlow that allow you to perform actions at various points in the test execution cycle, such as before or after each scenario or feature. This directory is where you'll define setup and teardown logic that applies across multiple tests.
  • Steps - The Steps directory contains the C# bindings for the steps defined in your feature files. Each step in a Gherkin scenario is mapped to a method in these files, containing the code that executes the step. This is where most of your test logic will reside, translating Gherkin steps into actions on your application.

Now that we have the template created, let's implement some code that we will add bdd tests for.

I've added a csproj called game, which includes a player. The player have a action called attack which attacks another player. Based on the player stats and class they have different stats, such as attack power, resistance and health.

namespace Game; public enum CharacterClass { Warrior = 1, Mage = 2 } public class Player { public Guid Id { get; } = Guid.NewGuid(); private int AttackPower { get; } private int Resistance { get; } private CharacterClass CharacterClass { get; } public double Health { get; private set; } public bool IsAlive => Health > 0; public Player(int attackPower, int resistance, double health, CharacterClass characterClass) { AttackPower = attackPower; Resistance = resistance; Health = health; CharacterClass = characterClass; } public void Attack(Player enemy) { var damage = CalculateDamage(enemy); if (damage > 0) { enemy.ApplyDamage(damage); } } private void ApplyDamage(double damage) => Health -= damage; private int CalculateDamage(Player enemy) { var bonusAttackPower = CharacterClass switch { CharacterClass.Mage => 20, CharacterClass.Warrior => 10, _ => 0 }; var enemyBonusResistance = enemy.CharacterClass switch { CharacterClass.Warrior => 10, CharacterClass.Mage => 5, _ => 0 }; var totalAttackPower = AttackPower + bonusAttackPower; var totalEnemyResistance = enemy.Resistance + enemyBonusResistance; return Math.Max(totalAttackPower - totalEnemyResistance, 0); } public void AdjustHealth(double amount) { Health += amount; } }

Before we add some scenarios for this, we should go through what the most common keywords inside a feature file is:

Feature - This keyword is used at the very beginning of a feature file to provide a name or a short description of the functionality or feature you are testing. It sets the context for the scenarios that follow.

Scenario - Defines a single scenario or user story that you want to test. It's a concrete example of how the feature should work under certain conditions.

Given, When, Then, And, But - These are step definitions that describe the steps of each scenario. They help break down the scenario into specific conditions (Given), actions (When), and outcomes (Then). "And" and "But" can be used for additional conditions, actions, or outcomes.

Scenario Outline - This keyword is used for parameterized tests, allowing you to run the same scenario with different sets of values. It's followed by an Examples section that contains a table of parameters to be tested.

Background - This keyword allows you to define steps that are common to all scenarios in the feature file. It helps avoid repetition by providing a shared context. Similar to NUnit [SetUp] attribute or XUnit constructor.

Tags - Tags categorize scenarios or features for selective execution. Prefixing with tags like @regression or @smoke allows grouping and targeted test runs, enabling flexible test suite management. You can also use @ignore to ignore a feature or scenario to be run.

Going back to the Player class we can see that there are some significant variation both when it comes to the CharacterClass and the flexible constructor. To accommodate all possible scenarios, using a parameterized approch may be the most sufficient. This is achieved through the use of the "scenario outline" keyword.

Here is an example:

bdd-scenario-outline

After defining our Scenario Outline in the feature file, the next part is to connect these scenarios to their respective step definitions in SpecFlow. Each Given, When, Then, And, and But in the Scenario Outline corresponds to a method in our test code, known as a step definition, which contains the actual implementation of the scenario.

Here is an example:

using Game.Specs.Drivers; using NUnit.Framework; namespace Game.Specs.Steps; [Binding] public class PlayerAttackSteps(GameDriver gameDriver) { [Given(@"the Mage has (.*) attack power, (.*) resistance, and (.*) health")] public void GivenTheMageHasAttackPowerResistanceAndHealth(int attackPower, int resistance, int health) { gameDriver.CreateMage(attackPower, resistance, health); } [Given(@"the Warrior has (.*) attack power, (.*) resistance, and (.*) health")] public void GivenTheWarriorHasAttackPowerResistanceAndHealth(int attackPower, int resistance, int health) { gameDriver.CreateWarrior(attackPower, resistance, health); } [Then(@"the Mage's health should be (.*)")] public void ThenTheMagesHealthShouldBe(double health) { Assert.AreEqual(health, gameDriver.GetMageHealth()); } [Then(@"the Warrior's health should be (.*)")] public void ThenTheWarriorsHealthShouldBe(double health) { Assert.AreEqual(health, gameDriver.GetWarriorHealth()); } [When(@"the Mage attacks the Warrior")] public void WhenTheMageAttacksTheWarrior() { gameDriver.MageAttacksWarrior(); } [When(@"the Warrior attacks the Mage")] public void WhenTheWarriorAttacksTheMage() { gameDriver.WarriorAttacksMage(); } [When(@"Mage attacks Warrior")] public void WhenMageAttacksWarrior() { gameDriver.MageAttacksWarrior(); } [When(@"Warrior attacks Mage")] public void WhenWarriorAttacksMage() { gameDriver.WarriorAttacksMage(); } }

The Given, When, Then attributes align with the contents of our feature file. Each time the feature is executed, these elements are parameterized and carried out, enabling our scenarios to flexibly adjust according to the inputs specified in our Examples table.

As we can see im using a driver, GameDriver. The main ide for a driver is to abstract the complexity of direct interactions with our Player class. This driver acts as a middleman, simplifying the process of setting up tests, executing actions, and verifying outcomes.

Here is an example of a driver:

namespace Game.Specs.Drivers; public class GameDriver { private Player Warrior { get; set; } private Player Mage { get; set; } public void CreateWarrior(int attackPower, int resistance, double health) { Warrior = new Player(attackPower, resistance, health, CharacterClass.Warrior); } public void CreateMage(int attackPower, int resistance, double health) { Mage = new Player(attackPower, resistance, health, CharacterClass.Mage); } public void WarriorAttacksMage() { Warrior.Attack(Mage); } public void MageAttacksWarrior() { Mage.Attack(Warrior); } public double GetWarriorHealth() => Warrior.Health; public double GetMageHealth() => Mage.Health; public void ResetGameState() { Warrior = null!; Mage = null!; } }

So far we have only covered Scenario Outline because it felt as a good match for our Player class. Lets have a look at another example that can benefit from keyword "background" and regular "scenario".

Lets say we have the following classes, that are adjusting the player benefits based on the environment:

namespace Game; public class Environment { public string Name { get; private set; } private EnvironmentalEffect EnvironmentalEffect { get; set; } public Environment(string name, EnvironmentalEffect environmentalEffect) { Name = name; EnvironmentalEffect = environmentalEffect; } public void ApplyEffectToPlayer(Player player) { player.AdjustHealth(EnvironmentalEffect.HealthAdjustment); } } public class EnvironmentalEffect { public double HealthAdjustment { get; set; } public EnvironmentalEffect(double healthAdjustment = 0.0) { HealthAdjustment = healthAdjustment; } } public static class GameEnvironments { public static readonly Environment HighGround = new("HighGround", new EnvironmentalEffect(healthAdjustment: 10)); public static readonly Environment InWater = new("InWater", new EnvironmentalEffect(healthAdjustment: -5)); }

Whenever a player is InWater there health benefit is reduced by -5 and while on HighGround a health bonus of 10 is added. The feature file for this looks like:

Feature: Environmental Health Effects To adapt to strategic advantages or challenges, Players receive health bonuses or suffer penalties based on their environment. Background: The player's health benefits is determined by environmental effects Given We have a Mage with 100 health Scenario: Player receives a health bonus on HighGround When the environmental effect HighGround are applied for the Mage Then the player's health should be 110 Scenario: Player suffers a health penalty in Water When the environmental effect InWater are applied for the Mage Then the player's health should be 95

Here, the Background keyword is useful. We set the mage health to 100 before each test runs allowing us to share a "given" between each scenario. We can also see two scenarios that captures both the test for HighGround and InWater which should be enough in this case.

Here is the steps file for the feature:

using Game.Specs.Drivers; using NUnit.Framework; namespace Game.Specs.Steps; [Binding] public class EnvironmentalHealthEffectsSteps(GameDriver gameDriver) { [Given(@"We have a Mage with (.*) health")] public void GivenWeHaveAMageWithHealth(int health) { gameDriver.CreateMage(0, 0, health); } [When(@"the environmental effect HighGround are applied for the Mage")] public void WhenTheEnvironmentalEffectHighGroundAreAppliedForTheMage() { gameDriver.ApplyMageEnvironmentHighGround(); } [When(@"the environmental effect InWater are applied for the Mage")] public void WhenTheEnvironmentalEffectInWaterAreAppliedForTheMage() { gameDriver.ApplyMageEnvironmentInWater(); } [Then(@"the player's health should be (.*)")] public void ThenThePlayersHealthShouldBe(int health) { Assert.AreEqual(health, gameDriver.GetMageHealth()); } }

Now that we've explored Features, Backgrounds, Scenarios, and Scenario Outlines, let's dig into generating documentation with the SpecFlow.Plus.LivingDocPlugin. This tool will transforms your BDD specifications into an interactive document. The document can be shared with stakeholders to help and ease in viewing the behavior specifications.

First, we need to install the SpecFlow.Plus.LivingDoc.CLI dotnet tool. This command-line interface tool allows us to generate the living documentation from our SpecFlow tests. Open your terminal or command prompt and run:

dotnet tool install --global SpecFlow.Plus.LivingDoc.CLI

To ensure the LivingDoc tool has been installed successfully, enter the following command:

livingdoc --help

This command should display a list of available commands and options for the livingdoc tool, confirming that it's ready for use.

Now, let's generate the living documentation HTML file. Replace [path to dll for Game.Spec] with the actual path to your SpecFlow project's compiled test assembly DLL file. For example, if your project is named Game.Specs and you've compiled it for .NET 6.0, your path might look something like .\Game.Specs\bin\Debug\net6.0\Game.Specs.dll.

livingdoc test-assembly [path to dll for Game.Spec]

This command will produce an HTML file named LivingDoc.html in the current directory (or the specified output path if you used the --output option).

Opening LivingDoc.html in will present you with view of your project's BDD specifications, including all feature tests and their parameters.

Here is an example image:

bdd-scenario-outline

Next part could be to integrated this into a CI/CD pipeline job that uploads the living document to a central location, making it easily accessible for all stakeholders.

Thats pretty much it for this post.

Happy coding!

please share