Close announcement
Back to blog

Specflow: 8 Steps to Use Behavior-Driven Development To Improve Team Performance

bug image

Let me tell you a story of a project of mine. We worked in a scrum team which consisted of software developers, testers, and undecided product owners. We would waste many hours per week waiting for the product owner to deliver the requirements.

We identified the problem. It was the product owner who wasn’t sure what the desired shape of our software should be. As a result, many questions from the development team remained unanswered. The product owner just said: I’m not sure what the requirements are.

The problem stemmed from the fact  that we didn’t write down the requirements. Instead, we only discussed them in meetings. In consequence, the requirements were missing a proper structure.

We could have worked faster if we had improved communication. And we found a solution: Behavior-Driven Development.

What is Behavior Driven Development?

Behavior Driven Development is an approach to software development in which we use concrete examples to specify how software should work. The purpose of BDD is to fill the communication gaps between businesses and software developers.

Behavior-Driven Development (BDD) vs. Test Driven Development (TDD)

BDD is an extension of TDD. There is an interesting article written by Dan North where he describes how BDD evolved from TDD: https://dannorth.net/introducing-bdd/

The main weakness of test-driven development is that traditional unit tests can’t be understood by business representatives. BDD solves this problem by introducing the domain-specific language (DSL). As a result, we can create real-life scenarios and discuss them with the Product Owner before even the development starts. In addition, BDD allows us to turn the scenarios into executable unit tests.

So BDD is a fusion of documentation and traditional tests. For this reason, it’s also called Living Documentation.

We successfully implemented BDD in our project. We prepared a lot of examples. The product owner could refer to each of them. Writing the requirements down was a great idea and removed any doubt. We started to receive clean and concrete requirements.

The product owner was happy to see examples and suggested updates almost immediately. Thereby our project took off. We had better requirements that we could turn into a code.

After coding is done, product owners or software testers conduct the acceptance tests. This phase of testing was much faster because we already had test scenarios, prepared in advance.

Behavior-driven development example & BDD process

Let’s discuss the process step by step. In this article, I’ll show you how to implement BDD in your project in 8 easy steps.

Step 1 - Learn Gherkin

Before we can start this phase we need to do some training in the team.

I made a presentation so that the entire team understood the new tool called Gherkin. Gherkin is a language that helps to write down documentation in the form of test scenarios.

Step 2 - Create test scenarios

Each programming language has its own BDD framework. In our case, we’ll use C#, where the most popular framework is Specflow. The easiest way to start is to install a Visual Studio extension called SpecFlow for Visual Studio 2022. You can install it directly from Visual Studio 2022 using the NuGet package.

75220200-e66d-4e3d-b154-981528155ff7.png

When you install this extension, you can create a new project type: Specflow Project. In a new project wizard, you can choose the .NET Framework version and a Test framework. Supported .NET versions from .NET Framework 4.6.1 to the most recent version .NET 5. Supported test frameworks are NUnit, xUnit, or MSTest. I use NUnit and the latest .NET 6.0 version.

Optionally, you can also add the FluentAssertions library to the project. It’s a library that helps you to write unit tests faster. You can recognize it by the Should()method in your test assertions. It makes unit tests assertions easier to read. We don’t use it in the examples below to focus only on the Specflow tool.

57950f80-fae8-4bad-b235-0365b3d79809.png

When the project is created, you’ll see that 4 folders have been generated:

ac128f9d-710f-48aa-8ebf-112d6b5dafb2.png

The most important folders are:

  • Features - this is where you can store your Gherkin scenarios. We store scenarios in .feature files.

  • StepDefinitions - here you can store the C# code for your tests

Let’s add a new feature by right-clicking the Features folder in the Solution Explorer and then Add -> New Item...

f9e6192c-5d84-41b8-a623-83dd674c1e52.png

Now let’s select Feature File for Specflow and give it the name LoanDecision.feature.

1bdc045b-4618-495c-acfa-db7779667bf5.png

When you click the Add button, Visual Studio will generate a new feature file with example content.

074042c7-ab6a-4a42-9e14-015b39a1bb9d.png

Let’s modify the scenario to make more sense. We will test a bank loan decision engine. For example, in Lloyds Bank, a customer needs to satisfy specific criteria to get a loan.

We can express all these criteria in a form of a Gherkin scenario. If you’re not familiar with the Gherkin syntax, visit this page https://cucumber.io/docs/gherkin/reference/

16065255-cea1-4040-81d7-bd790e9ee650.png

Feature: Loan Decision

As a bank employee
I need to calculate a decision for an individual consumer
To grant or reject a loan

Scenario: Positive decision
Given an individual customer is aged 18
And a customer is a UK resident
And a customer has held a Lloyds Bank current account for at least one month
And a customer has a regular income
And a customer is not a full-time student
And a customer has no bad credits
When a customer applies for a £34,000 loan
Then decision should be positive

Scenario: Negative decision - bad credits
Given an individual customer is aged 18
And a customer is a UK resident
And a customer has held a Lloyds Bank current account for at least one month
And a customer has a regular income
And a customer is not a full-time student
But a customer has 1 bad credit
When a customer applies for a £34,000 loan
Then decision should be negative

Scenario: Negative decision - younger than 18
Given an individual customer is aged 17
And a customer is a UK resident
And a customer has held a Lloyds Bank current account for at least one month
And a customer has a regular income
And a customer is not a full-time student
But a customer has 0 bad credit
When a customer applies for a £34,000 loan
Then decision should be negative

Scenario: Negative decision - an amount higher than 35k
Given an individual customer is aged 17
And a customer is a UK resident
And a customer has held a Lloyds Bank current account for at least one month
And a customer has a regular income
And a customer is not a full-time student
But a customer has 0 bad credit
When a customer applies for a £36,000 loan
Then decision should be negative

Now we have test cases containing many lines of human-readable text. This is handy because at this stage, software developers can speak the same language as product owners. It greatly improves communication when developers create test scenarios and share them with product owners. For example, you can commit Gherkin scenarios to your git repository and ask product owners to look at it and suggest changes.

Our BDD adoption went very smoothly because after 1 week, our product owner, a non-technical person, was able to update Gherkin scenarios and commit changes to the git repository.

Step 3 - Create steps definitions

The next step is to generate step definitions. With the Specflow we can make it directly from Visual Studio.

e4e4d7ce-0751-460b-83ea-07a4c039fb99.png

The Specflow extension helps you to generate a new C# method for each line of the Gherkin scenario. We call this method a test step definition. In the beginning, each method is empty. It is a developer’s  task to add a code to it. You can prepare test scenarios, execute some actions or add an assertion.

When  the steps are filled with code it may look like this:

using System;
using TechTalk.SpecFlow;
using DecisionEngine;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
namespace DecisionEngineTest.StepDefinitions
{
    [Binding]
    public class LoanDecisionStepDefinitions
    {
        private LoanApplication application = new LoanApplication()
        {
            Customer = new Customer()
            {
                Accounts = new List<Account>()
                {
                    new Account()
                }
            }
        };

        [Given(@"an individual customer is aged (.*)")]
        public void GivenAnIndividualCustomerIsAged(int age)
        {
            var now = DateTime.UtcNow;
            application.Customer.BirthDate = new DateOnly(now.Year-age, now.Month, now.Day);
        }

        [Given(@"a customer is UK resident")]
        public void GivenACustomerIsUKResident()
        {
            application.Customer.IsUkResident = true;
        }

        [Given(@"a customer has held a Lloyds Bank current account for at least one month")]
        public void GivenACustomerHasHeldALloydsBankCurrentAccountForAtLeastOneMonth()
        {
            application.Customer.Accounts.First().CreateDate = DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-2));
        }

        [Given(@"a customer has a regular income")]
        public void GivenACustomerHasARegularIncome()
        {
            application.Customer.Accounts.First().LastIncomeTransfer = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-20));
        }

        [Given(@"a customer is not a full-time student")]
        public void GivenACustomerIsNotAFull_TimeStudent()
        {
            application.Customer.IsFullTimeStudent = false;
        }

        [Given(@"a customer has no bad credits")]
        public void GivenACustomerHasNoBadCredits()
        {
            application.Customer.BadCreditsCount = 0;
        }
        [When(@"a customer applies for a £(.*) loan")]
        public void WhenACustomerAppliesForALoan(Decimal amount)
        {
            application.Amount = amount;
        }
        [Then(@"decision should be positive")]
        public void ThenDecisionShouldBePositive()
        {
            Assert.AreEqual(LoanDecision.Acceptance, application.GetDecision());
        }

        [Given(@"a customer has (.*) bad credit")]
        public void GivenACustomerHasBadCredit(int badCredits)
        {
            application.Customer.BadCreditsCount = badCredits;
        }
        [Then(@"decision should be negative")]
        public void ThenDecisionShouldBeNegative()
        {
            Assert.AreEqual(LoanDecision.Denial, application.GetDecision());
        }
    }
}

The code of the application that we are testing looks like this:

namespace DecisionEngine
{
    public enum LoanDecision
    {
        Acceptance,
        Denial
    }
    public class LoanApplication
    {
        public Customer Customer { get; set; }
        public decimal Amount { get; set; }
        public LoanDecision GetDecision()

        {
            var accountOlderThan30Days = (DateTime.UtcNow - Customer.Accounts.First().CreateDate.ToDateTime(TimeOnly.MinValue)).TotalDays > 30;
            var hasRegularIncome = Customer.Accounts.Any(x => (DateTime.UtcNow - x.LastIncomeTransfer.ToDateTime(TimeOnly.MinValue)).TotalDays < 40);
            var isAdult = Customer.BirthDate.ToDateTime(TimeOnly.MinValue).AddYears(18) < DateTime.UtcNow;

            if (Customer.IsUkResident && accountOlderThan30Days && hasRegularIncome &&
                !Customer.IsFullTimeStudent && Customer.BadCreditsCount == 0 && Amount <= 35000 && isAdult)
            {
                return LoanDecision.Acceptance;
            }
            return LoanDecision.Denial;
        }
    }

    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public List<Account> Accounts { get; set; }
        public DateOnly BirthDate { get; set; }
        public bool IsUkResident { get; set; }
        public bool IsFullTimeStudent { get; set; }
        public int BadCreditsCount { get; set; }
    }

    public class Account
    {
        public string Id { get; set; }
        public decimal Money { get; set; }
        public DateOnly LastIncomeTransfer { get; set; }
        public DateOnly CreateDate { get; set; } = new DateOnly();
    }
}

Step 4 - Connect to your favourite testing framework in development process

When it comes to assertions, we need to choose a testing framework. For .NET there are a few to choose from XUnit, NUnit, or MSTest. Whichever you choose, you’ll use it mainly for creating assertions in your step definitions. You can choose a test framework when you create a new project.

57950f80-fae8-4bad-b235-0365b3d79809 (1).png

Specflow will also use it to turn Gherkin scenarios into tests, but it’s an automatic process and you don’t need to bother.

The Specflow extension has a great feature that helps to generate steps definitions from Visual Studio. All you need to do is to right-click on the Gherkin scenario and choose Define Steps…

e4e4d7ce-0751-460b-83ea-07a4c039fb99 (1).png

Then you’ll see the preview of the steps to be generated.

8397b97b-0c5a-4ec7-bf8b-29171fa3cf35.png

Click Create button and a new C# class will be created.

c7369760-9af9-4331-a808-4fbb6b0ee049.png

Each line of the scenario has been turned into a method. Please note the attributes: [Binding], and [Given]. They’re used to create a relation between a specific line of text in a feature file and a C# method. 

Step 5 - Implement Page Object model

The page object is a well-known design pattern. It’s most popular among professional software testers. You can use it when you test websites. Page Object helps you to create an abstraction over HTML elements. For example, when you test an online shop website, then you can create a class called MainPage. In this class, you can add properties like SearchTextBox, SearchButton, ProductList, and so on. You probably have noticed that each property serves as an interface to a specific page element. As a result, our tests can interact with a website in an object-oriented way.

Please bear in mind that this is an optional step and it only applies to user interface tests. When you test REST API, you don’t need to think about the page objects. You can read more about the Page Object on Martin Fowler website: https://martinfowler.com/bliki/PageObject.html

You can also read John Ferguson Smart’s article to understand it better: https://johnfergusonsmart.com/page-objects-that-suck-less-tips-for-writing-more-maintainable-page-objects/

Step 6 - Run Tests Locally

Now, you are ready to execute the test. You can do it just like you would run unit tests. You can do it from Visual Studio. Just open the Test Explorer and run the tests.

460dfb14-2a4a-46a7-a10d-95b97954a432.png

Step 7 - Create a Test Execution Report

The significant benefit of using the BDD framework is generating test reports. Most importantly, we can create an HTML report which shows which Gherkin scenarios has failed. It also shows the percentage of the successful tests. The desired number is always 100% which means that the software is ready to be deployed to the production environment at any time. You can generate a Specflow HTML report using the livingdoc command-line tool. To install it, you should run this command line:

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

The command which generates HTML looks like this:

livingdoc test-assembly C:\Users\darek\source\repos\DecisionEngineTest\DecisionEngineTest\bin\Debug\net6.0\DecisionEngineTest.dll -t C:\Users\darek\source\repos\DecisionEngineTest\DecisionEngineTest\bin\Debug\net6.0\TestExecution.json

The livingdoc command takes 2 parameters: test assembly path and a test execution JSON file which is generated after tests execution in Visual Studio.

HTML report:

7b907f88-389f-44bf-9213-fbc2f2989a5d.png

In the HTML report,  you can open the Analytics tab where you see the number of features, scenarios, and tests. You can also see how many steps have not been used. This feature helps you to remove a redundant code from your solution.

1469e1dd-cd85-43fa-a561-ab8ebf2e8cdd.png

Step 8 - Run your tests on the build server

Test reports are especially useful in automated regression tests. These tests typically take a few hours, so they can be triggered at night. In the morning, the software developers can see what’s wrong and fix the software to test it again. In big projects, we have a QA Engineer, a person dedicated to  maintaining automated tests. The automation itself can be created with Jenkins, Azure Pipelines, TeamCity or any other build server.

If you follow these 8 easy steps, you have a working test  that is understood by developers and product owners. Remember that the main purpose of BDD is communication improvement. The unit tests are just a side effect.

If you’re interested in adopting BDD in your project, you should visit https://specflow.org/ and learn how Specflow can improve your team performance and communication with product owners.

Benefits of Behavior Driven Development

BDD focuses on the behavior of the software and uses scenarios and examples to ensure that the software is built to meet business requirements and specification. Here are some of the benefits of BDD:

  1. Improved collaboration and communication BDD encourages collaboration and communication among developers, testers, and business stakeholders. The shared understanding of the behavior of the software helps to identify potential issues early in the development process and ensures that the software meets business requirements.

  2. Faster feedback and faster delivery BDD allows for faster feedback and faster delivery by providing a clear understanding of the expected behavior of the software. The use of scenarios and examples ensures that the software is tested thoroughly, which reduces the risk of defects and increases the speed of delivery.

  3. Improved quality and reduced defects BDD ensures that the software is built to meet business requirements and specification by using scenarios and examples to test the behavior of the software. This approach leads to improved quality and reduced defects, as the software is tested thoroughly before being released.

  4. Reduced rework and maintenance costs BDD helps to reduce rework and maintenance costs by ensuring that the software is built to meet business requirements. By testing the behavior of the software, BDD ensures that defects are caught early in the development process, reducing the need for costly rework and maintenance.

  5. Increased customer satisfaction BDD ensures that the software is built to meet business requirements and provides a clear understanding of the expected behavior of the software. This leads to increased customer satisfaction, as the software meets the needs of the business and delivers value to the customer.

In conclusion, Behavior-Driven Development is an effective approach to software development that emphasizes collaboration and communication among developers, testers, and business stakeholders. BDD ensures that the software is built to meet business requirements, tested thoroughly, and delivered quickly and efficiently.

By adopting BDD, software development agile teams can improve quality, reduce defects, and increase customer satisfaction while reducing rework and maintenance costs.

Happy (automated) testing!

Speed up the entire testing process now

Automate web app testing easier than ever. Without excessive costs. Faster than coding. Free forever.
Dariusz Kacban photo

Dariusz Kacban

.NET Developer

linkedin.com/in/darekkacban

Don't miss any updates
Get more tips and product related content. Zero spam.