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.
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.
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
_result replace the usages of
ScenarioContext. The dictionary keys and the list initialization code go away, and the step methods clean up nicely.
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.
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.