We need to access different services and data from multiple step definition classes. Shall we use base classes or context injection for that? — This question has popped up at two of my clients, so I thought it would be worth discussing it also in a blog post.
Step definitions are global in SpecFlow, just like in any other Cucumber-family BDD tool. This means that you can place them into any class, which is marked with the
[Binding] attribute, SpecFlow will find them when executing the scenario steps.
Let’s say you have two step definitions and both have to access shared data (state), the
UserContext class instance, which is a state bag for holding the name, and the role of the current user. The
UserContext class is stateful as it stores this information in instance fields.
Depending on the fact whether the two step definitions are in the same class or not, you can choose from different options.
If they are in the same class, you can create an instance field for the
UserContext in the step definition class, initialize them in the constructor and simply use them in the step definitions. (In this example, we store the
UserContext instance in the field
_userContext, but in many cases you would store the user name and the role directly as fields.)
As SpecFlow creates a new instance of the step definition class for every scenario, this is a safe and efficient solution.
If the step definitions are in different classes, the situation is not that easy, because you cannot use the instance fields to communicate between the step definitions. In this case, you have three choices, as it is described in the SpecFlow documentation as well.
- Use static fields (this will cause troubles if later you want to run your tests in parallel)
- Use the current
ScenarioContextto store and access the data
- Use context injection to inject the same
UserContextinstance to both classes
Here is an example for the
Note: The property could access the
ScenarioContext.Current static accessor as well, but this would not work in parallel execution. The example above uses the
ScenarioContext property declared in the
Steps base class of the SpecFlow runtime library. This property also gives you the current scenario context, but it works in parallel execution as well.
And this is the same example using context injection.
What about using a base class?
You might think about another option: create a base class and declare the
_userContext field there. Unfortunately, although you can create a common base class for your step definition classes, but you cannot use it for sharing state. The reason for this is the following: if you declare an instance field in the base class, it will be inherited into your derived step definition classes; when SpecFlow creates instances of them, each will have its own field, so basically they will not “see” the data of each other.
You can combine the base classes with the
ScenarioContext approach, however. For that, you need to declare properties in the base class that exposes the state stored in the current scenario context. In our case, you can declare a read-only property of type
UserContext that reads the user context from the
ScenarioContext and creates a new one if it was not in the scenario context before. This property does not have a backing field, so it does not store any data directly in the step definition class itself, so there will be no data duplication.
Injecting context to base classes
The ones who prefer base classes (with the
ScenarioContext approach) will probably choose this option, because this way the step definition classes become slightly simpler — they don’t need either a constructor or the fields for holding the injected contexts.
Context injection provides many benefits over the
ScenarioContext approach, like handling more complex dependencies, taking care of the creation of the objects and disposing them at the end of the scenario.
With a small trick, you can also use base classes together with context injection. For this purpose, the read-only properties you declare in the base class should simply use the object container of the scenario execution to resolve the context object. The object container can be accessed from the
Base context vs. injecting dependencies to the step definition classes
We have seen that the dependencies of the step definition can be injected both to a base class or directly to the step definition classes. Which one is better then?
In my opinion, there is no better or worse option, but they represent different architectural models.
In the case of the base class, the different dependencies of the different step definition classes will be added to the base class. In the end, it will become a kind of central hub that keeps all dependencies together. This is a centralized model. For smaller and simpler projects, in which the number of dependencies is limited, it can serve well, but for larger projects, the continuously growing base class might cause maintenance issues.
Injecting the dependencies to the step definition classes represents a decentralized model. Every step definition class can list its dependencies in the constructor and be independent from any other shared service or data classes. If the step definitions are grouped into classes along with the dependencies they use, one class will have only a few dependencies. The classes that depend on the same shared services or state will form a cluster. In our example, you can easily see which step definitions need to deal with the current user. This model adds a slight overhead to the step definition classes (they need the constructor and the fields for storing the dependencies), however, it can better support larger or more complex automation solutions.
Both base classes and context injection can be useful for accessing shared services or data. Use context injection for larger or more complex projects. Use base classes when the simplicity of the step definition classes is important or use them for smaller projects, when the number of dependencies is low. Even in this case, I recommend to use the base class in a way that it resolves the dependencies from the object container.