Specflow

Whats Specflow?

Its a library that allows for Behavior Driven Development in .NET. (BDD) using Gherkin Syntax (Given-When-Then).

“SpecFlow helps teams bind automation to feature files and share the resulting examples as Living Documentation across the team and stakeholders. - specflow.org”

PRO - using Gherkin and Specflow allows the entire team to contribute towards the test requirements, your Product Owner or Designer may not be technical but they do understand the domain so dont discount their input.

CON - Gherkin and Specflow add additional complexity, if its just the developer writing the tests then its probably not going to add much value.

SpecFlow seems pretty focused on integration testing - Code that invokes a unit of work that crosses project boundaries, uses actual external dependencies, and/or validates many different aspects about the code under test.

As Im using Visual Studio 2019 I installed the extension SpecFlow for Visual Studio 2019.

Whats Gherkin?

Its the Syntax (Given-When-Then) which are keywords, the example below is from the template SpecFlow Project

1
2
3
4
5
6
7
8
9
10
Feature: Calculator
In order to avoid silly mistakes
As a math idiot
I *want* to be told the **sum** of ***two*** numbers

Scenario: Add two numbers
Given the first number is 50
And the second number is 70
When the two numbers are added
Then the result should be 120

Specflow then knows how to translate the above into steps simliar to the classic tripple A testing pattern.

All of the below will have using TechTalk.SpecFlow; this resolves the annotations Given When Then above the methods.

If you place your cursor on a line in the .feature and press F12 (Go to definition) the IDE should navigate to the correct place in the step as shown in CalculatorStepDefinitions.cs below.

Arrange

1
2
3
4
5
6
7
8
9
// Given the first number is 50

[Given("the first number is (.*)")]
public void GivenTheFirstNumberIs(int number)
{
//TODO: implement arrange (precondition) logic

_scenarioContext.Pending();
}

Additional Arrange steps are added with the keyword And:

1
2
3
4
5
6
7
8
9
// And the second number is 70

[Given("the second number is (.*)")]
public void GivenTheSecondNumberIs(int number)
{
//TODO: implement arrange (precondition) logic

_scenarioContext.Pending();
}

Act

1
2
3
4
5
6
7
8
9
// When the two numbers are added

[When("the two numbers are added")]
public void WhenTheTwoNumbersAreAdded()
{
//TODO: implement act (action) logic

_scenarioContext.Pending();
}

Assert

The assertion can be done with any testing framework or the SpecFlow+ Runner.

1
2
3
4
5
6
7
8
9
// Then the result should be 120

[Then("the result should be (.*)")]
public void ThenTheResultShouldBe(int result)
{
//TODO: implement assert (verification) logic

_scenarioContext.Pending();
}

Sweet how do I set things up?

The flow is simply and there are awesome getting started guides like Getting Started With An Example and Getting Started With A New Project.

A summary of the core steps are with a new project are:

  • Create new project from template SpecFlow Project
  • Create the Example Project, this is the code you want to test, following their example its a class library. EACH LINE in the Scenario needs to be satisfied with c# code:
    • public void GivenTheFirstNumberIs(int number), this sets (arrange) the state FirstNumber
    • public void GivenTheSecondNumberIs(int number) this sets (arrange) the state for SecondNumber
    • public void WhenTheTwoNumbersAreAdded() this calls some behaviour (act) : _calculator.Add();
    • public void ThenTheResultShouldBe(int expectedResult) asserts the result

Scenario Context

See ScenarioContext

The boilerplate code injected ScenarioContext _scenarioContext; into the constructor in CalculatorStepDefinitions.cs

“ScenarioContext helps you store values in a dictionary between steps. This helps you to organize your step definitions better than using private variables in step definition classes. -docs.specflow.org”

This means we call things like

1
2
3
_scenarioContext.Add   ~ add new key/value
_scenarioContext.Get ~ get value by key
_scenarioContext.Set ~ update value by key

They also have something called Context-Injection

“This feature allows you to group the shared state in context classes, and inject them into every binding class that needs access to that shared state. -docs.specflow.org”

To use context injection:

  1. Create your POCOs (simple .NET classes) representing the shared data.
  2. Define them as constructor parameters in every binding class that requires them.
  3. Save the constructor argument to instance fields, so you can use them in the step definitions.

This means CalculatorStepDefinitions could be changed from creating its own new Calculator();

1
2
3
4
5
6
7
8
9
10
private readonly ScenarioContext _scenarioContext;
private readonly Calculator _calculator;

private int _result;

public CalculatorStepDefinitions(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
_calculator = new Calculator();
}

To automagically injecting an instance based on the default constructor

1
2
3
4
5
6
7
private readonly Calculator _calculator;
private int _result;

public CalculatorStepDefinitions(Calculator calculator)
{
_calculator = calculator;
}

Feature Context

See FeatureContext

“FeatureContext persists for the duration of the execution of an entire feature, whereas ScenarioContext only persists for the duration of a scenario. -docs.specflow.org”

These are useful hooks as you can have annotations like [BeforeFeature] and [AfterFeature].
Note that hooks also exist for Scenario Context: [BeforeScenario] and [AfterScenario].

You can use either to rebind/override IKernel with your own mock implementations.

Example from docs.specflow.or:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Binding]
public class Hooks
{
[BeforeFeature]
public static void BeforeFeature(FeatureContext featureContext)
{
Console.WriteLine("Starting " + featureContext.FeatureInfo.Title);
}

[AfterFeature]
public static void AfterFeature(FeatureContext featureContext)
{
Console.WriteLine("Finished " + featureContext.FeatureInfo.Title);
}
}

References