SpecFlow is an extension for Visual Studio that binds software specifications written in the Gherkin language to executable code written in C#, VB, or some other .NET language. One of the challenges of implementing a SpecFlow scenario is how to manage test state in between steps. At first glance, state management seems like an easy problem to solve, but as your test suite grows, maintaining the implementation is as difficult as any large software project.
For this post, I’m going to run through the different state management mechanisms I’ve encountered in my SpecFlow experience and review the pros and cons of each. I’ll use the default calculator feature generated by SpecFlow to illustrate the different approaches.
The service under test will be a straightforward calculator service which uses a single method Add()
to find the sum of a list of numbers.
Scenario Context
The first state management mechanism uses a SpecFlow runtime construct called the scenario context. It is an object that contains a state bag which persists in memory for the lifetime of an executing scenario. It is accessible via the static ScenarioContext.Current
property, and each test step can manipulate the scenario context state bag by adding/removing/updating members.
Below is a step definition file that binds to Calculator.feature
. It uses ScenarioContext
to store the numbers entered into the calculator as well as the result. Later it retrieves them to perform the addition logic and verify the result.
While the scenario context state bag is convenient and easy to use, it forces an explicit dependency on the SpecFlow runtime. Also, maintaining the dictionary keys becomes a hassle for large codebases.
Private Members
Since the SpecFlow runtime reuses the same instance of a step definition class for a scenario, you can save state in between method invocations by writing to private member variables. This is a improvement over using ScenarioContext
as it significanty reduces the amount of code in a step definition class. The downside is the state cannot be shared with other step definition classes.
In the example, two private fields _values
and _result
replace the usages of ScenarioContext
. The dictionary keys and the list initialization code go away, and the step methods clean up nicely.
Context Object
You can get around the limitations of private member variables by grouping related state into a context object. Then you can use the SpecFlow runtime’s IoC container to inject the object into whatever step needs it. The CalculatorContext
class contains the list of integers to add and a single integer to hold the result. An instance is injected into the constructor of CalculatorSteps
and stored as a single private field. Other step implementation classes can ask for the same type in their constructors, and they will get the same shared instance.
Domain Object
You can take the context object approach one step further and include the behavior that exercises CalculatorService
in the object itself. For example, the method CalculatorSteps.WhenIPressAdd()
can be extracted from the steps class and moved to the new Calculator
class. The result is the state and behavior are encapsulated in one place, and an instance of Calculator
can be injected into each step definition class via the IoC container. The step definition becomes a thin layer with two responsibilities: invoking the test code in Calculator
and verifying the result.
Of these four state management techniques, I prefer using private member variables for small simple tests and domain objects for complex tests with multiple step definition classes. I avoid using ScenarioContext
as it doesn’t scale well for large test suites.