Dependency Injection and Context Injection

July 12, 2023

Inversion of control and dependency injection are big topics. I'm going to talk you through a high level overview of what Dependency Injection is and take you through the specific use case that SpecFlow refers to as Context Injection.

Context Injection with SpecFlow

Inversion of Control, Dependency Injection and Context Injection

Inversion of Control (IoC)- A design principle (high level concept) used to achieve loose coupling of objects and creation of dependant objects.

Dependency Injection (DI) – A design pattern (specific approach) that implements Inversion of Control (IoC).

Context Injection – SpecFlow's specific implementation of Dependency Injection.

So just think of it as a hierarchy. At the top you have the high level concept of Inversion of Control. Dependency Injection is a specific approach implementing Inversion of Control. And Context Injection is SpecFlow's specific implementation of Dependency Injection.

You'll find SpecFlow documentation using the term 'Context' a lot to. The definition for Context being…

Context

“Circumstances that form the setting for an event, statement, or idea, and in terms of which it can be fully understood."

In the SpecFlow domain just think of “Context" as the 'data' and/or 'state' that is needed for our test scenario. We need to inject the context into our test scenarios. Or you could rephrase this as “We need to SET the DATA/STATE for our test scenario". And we'll do this with “Context Injection".

There are a LOT more terms and concepts in this domain. For the purpose of understanding and using Dependency Injection in SpecFlow, being familiar with these three terms will cover most of what you need.

Why We Need Dependency Injection and Context Injection

At the simplest level think of it like this..

We have a main class (could be a Step Defintion class or a Hooks class).

We have another class that provides context or service.

This first class (Step Definition Class) is dependent on the second class. It's dependent because that class stores some context or data you need during your test scenario. In a basic setup the dependent class (Step Definition Class) will create an instance of the class it's dependent on (Context Class).

Class Dependency

Now let's say we have multiple step definition classes and a hooks class. They all need this data/context. Not only do they need it but they need to share ONE instance of this of this class. They do NOT want to be creating a new instance every time they need it. The classes that are DEPENDENT on the “context class" want to share one instance.

Many Classes Dependent on Context Class

We now have many classes that are dependent on the one instance of “Context Class". All the dependent classes want to share the data that is contained in the “Context Class". That's fine except that we now run into four problems.

1. Which class should create the initial instance of the “Context Class"?

2. How do the other classes know if the “Context Class Instance" has already been created?

3. What happens if no dependent classes are using the context but we still want to maintain it?

4. How do we know when to dispose of the “Context Class Instance"?

Well guess what? We need a “Dependency Injector"!

What Is A Dependency Injector?

The challenge is how do we create the first AND ONLY instance of this context class and then share it between other classes. For example share it between all our step definition classes. We need a clever way to hand off the creation and management of this class (our Context class) to another entity. That entity is our Context Injector.

Context Injector

In short this little box of tricks (the injector) is responsible for creating the instance of the 'Context' class and 'injecting' the Context class into any other classes that might need it.

This injector class delivers 4 things for us ..

1. CREATE: when an insance of the class doesn't exist the Injector creates that first instance.

2. MAINTAIN: when one dependent class (e.g. a step defintion class) has finished with the Context class and we want to keep one insance in play (ie. not dispose of it).

3. PROVIDE/INJECT: to provide the 'in play' instance of the context class to any other client/class when it needs it. MANY dependent classes can have the ONE instance of the Context class injected.

4. DISPOSE: to dispose of the context class instance when, AND only when, we're ready.

So rather than create this 'service' directly from the dependent class we hand the creation of the Context class off to an INJECTOR. In SpecFlow this Injector capability is provided by the inbuilt BoDi libraries (don't worry too much about the BoDi libraries this is all hidden from you in SpecFlow).

Now we understand why we need Context Injection we need to know how to implement it in our code. You can implement in two ways…

1. Constructor Injection

2. Parameter Injection

We'll take a look at each of these in turn.

First though let's relate this to a SpecFlow project in Visual Studio so that you can see how things fit together.

Structure and Relationships

Let's take the key bits from a demo SpecFlow project. The three components we're interested in are:We'll take a look at each of these in turn.

First though let's relate this to a SpecFlow project in Visual Studio so that you can see how things fit together.

1. Step Definition Classes : in our example we have 3 step definition files/classes that are define the steps for testing some simple Calculator application. All 3 step definition files are dependent on a CalcContext class that contains data/state information during the test run.

2. CalcContext Class : this is a class we've created to store some data/state information during the test run. We want to create one (and only one) instance of this class and share it between all the Step Definition classes.

3. Injector : the Dependency Injection feature that comes with SpecFlow is a class called BoDi. It's included in the SpecFlow package. This is implemented behind the scenes in SpecFlow but we're showing it here so that you can see how everything works.

SpecFlow Context Injection Example

Next we need to understand more about this CalcContext class that we want to Inject into our Step Definitions. Well in our example this is a simple class with a few fields


namespace SpecFlowProject2
{
    public class CalcContext
    {
        public double CurrentResult { get; set; }
        public string? CurrentMode { get; set; }
    }
}

It's just tracking the Current Result and the Current Mode values. Two fields containing values/data that we want to share between our Step Definitions during the test run. Note, for simplicity and clarity we've kept this Context class really basic here.

As I mentioned above there's two ways to get the Injector to inject the Context into the dependent classes

1. Constructor Injection

2. Parameter Injection

SpecFlow Context Injection options

First up then let's see how we'd inject an instance of this CalcContext class into our Step Definitions using Constructor Injection then.

Constructor Injection

In order to inject an instance of the Context class into our Step Definitions we need to do five things in our Step Definition class.


using CalculatorSpecFlow;
using BoDi;   //  ** FIRST **

namespace SpecFlowProject2.StepDefinitions
{
    [Binding]
    public sealed class CalculatorStepDefinitions
    {
        private readonly Calculator _calculator;
        private CalcContext _calcContext;   // ** SECOND **

		// ** THIRD **
        public CalculatorStepDefinitions(CalcContext calcContext, Calculator calculator)
        {
            _calculator = calculator;
            _calcContext = calcContext;  // ** FORTH **
        }

        [Given("the first number is (.*)")]
        public void GivenTheFirstNumberIs(int number)
        {
            _calculator.FirstNumber = number;
            _calcContext.CurrentMode = "standard";  // ** FIFTH **
        }

        [Given("the second number is (.*)")]
        public void GivenTheSecondNumberIs(int number)
        {
            _calculator.SecondNumber = number;
        }


        [Then("the result should be (.*)")]
        public void ThenTheResultShouldBe(int result)
        {
            _calcContext.CurrentResult.Should().Be(result);
        }
    }
}

Let's look at each piece in turn:

__FIRST__


using BoDi;   //  ** FIRST **

__SECOND__


private CalcContext _calcContext;   // ** SECOND **

__THIRD__


public CalculatorStepDefinitions(CalcContext calcContext, Calculator calculator)

This is where the magic happens. The Injector (BoDi) sees the 'CalcContext' class defined as a parameter in the Constructor. This is the signal to the injector (BoDi) to inject an instance of the CalcContext class. There are two scenarios here…

a. If an instance of CalcContext doesn't already exist then the Injector will create a single Instance

b. If the Injector has already created an instance of the CalcContext class, and it's already holding/tracking that instance, then it will just pass the existing instance in via the constructor.

__FORTH__


_calcContext = calcContext;  // ** FORTH **

In the body of the constructor we take the instance of the calcContext that the injector has passed in and save it in our `_calcContext` instance field. This makes the calcContext object available throughout our Step Definition class as the field `_calcContext`.

__FIFTH__


  _calcContext.CurrentMode = "standard";  // ** FIFTH **

Now that we have the CalcContext object safely stored in our `_calcContext` field we can use in our Step Definitions. In this example we're setting the 'CurrentMode' field in our CalcContext object.

Any other Step Definition class that uses the same Constructor Injection with CalcContext will get the SAME instance of the CalcContext object from the Injector. So let's say in another Step Definition class we have..


using CalculatorSpecFlow;
using BoDi;

namespace SpecFlowProject2.StepDefinitions
{
    [Binding]
    public sealed class CalculatorAddStepDefinitions  // <== Add Step Defintions
    {

        private readonly Calculator _calculator;
        private CalcContext _calcContext;

        public CalculatorAddStepDefinitions(CalcContext calcContext, Calculator calculator)
        {
            _calculator = calculator;
            _calcContext = calcContext;
        }

        [When("the two numbers are added")]
        public void WhenTheTwoNumbersAreAdded()
        {
            _calcContext.CurrentResult = _calculator.Add();
            Console.WriteLine("in mode: " + _calcContext.CurrentMode);  // <====
        }

    }
}

You'll see in this Step Definition class that we use the same constructor injection approach. We pass the CalcContext object in as a parameter in the constructor. Now because the Injector, injects the existing CalcContext object we can use statements like…


Console.WriteLine("in mode: " + _calcContext.CurrentMode);  // <====

This uses the already set value for 'CurrentMode' and will print “in mode: standard" to the console.

_Note that with this 'Constructor Injection' approach we're adding the Context class to an “Instance Variable“. This makes the context class available to the WHOLE class._

If you want to be more targeted and only use the context class in one or two methods then we can use Parameter Injection.

Parameter Injection

When you inject the Context with the constructor you make the Context available to the whole class (as an instance variable). If the Context is only relevant to one (or possibly a handful) of methods in the class then it make more sense to inject as a parameter in the specific method that needs the Context.

Let's take an example where we have “Before Scenario" hook. This hook is tagged so that it only runs when we run tests where the calculator mode needs to be in 'Standard' mode before these scenarios run. So we have a hook class like this…


namespace SpecFlowCalculator2
{
    [Binding]
    public sealed class Hooks
    {
        [BeforeScenario("@standard_mode")]
        public void BeforeScenario()
        {
           // Do something before 'Sandard Mode' scenarios start
        }
    }
}

Now remember that our CalcContext object contains the details about the mode that our calculator object is running in. The 'currentMode' property is shown in the CalcContext class below.


namespace SpecFlowProject2
{
    public class CalcContext
    {
        public double CurrentResult { get; set; }
        public string? CurrentMode { get; set; }   // <=== Sets Current Mode
    }
}

Our “Current Mode" property tracks which mode our calculator is in (e.g. Standard mode or Scientific mode). We're looking to ….

a. set this property at the start of all our scenarios that are 'standard mode' scenarios

b. use this property when needed in all our step defintions that execute the test steps

Let's use 'Parameter Injection' in our Hook file to set the mode automatically at the start of the scenarios. _Now we're using Parameter Injects (not Constructor Injection) because this context is only really relevant to this one method in the Hooks Class._ If all the methods in this Hooks class needed this Context then we'd use Context Injection in the constructor.

So to re-iterate, only this Before Scenario method needs this Context so we'll use Parameter Injection. We update the Hooks class as follows:


using BoDi;  //  ** FIRST **

namespace SpecFlowCalculator2
{
    [Binding]
    public sealed class Hooks
    {
        [BeforeScenario("@standard_mode")]
        public void BeforeScenario(CalcContext calcContext)  // ** SECOND **
        {
            calcContext.CurrentMode = "standard";  // ** THIRD **
        }
    }
}

__FIRST__


using BoDi;   //  ** FIRST **

We need to reference the BoDi namespace that acts as the “Injector". Again, we don't need to do this… it's done automatically in later versions of SpecFlow. I include it here so that you can see how we link the Injector class (BoDi) to the Step Definitions.

__SECOND__


	public void BeforeScenario(CalcContext calcContext) // ** SECOND **

In the parameters for this method we pass in the calcContext object (which is of a type CalcContext).

__THIRD__


calcContext.CurrentMode = "standard";  // ** THIRD *

We set the 'CurrentMode' property in our calcContext object. The calcContext object is available in this method because the injector passed it in as the parameter in the method definition.

You can see that Parameter Injection is a bit simpler than Constructor Injection. However, if you're re-using the Context in lots of methods within the same class then it's neater to implement Context Injection and use the Instance Variable that holds the context object.

SUMMARY

In short we need Dependency Injection because we want to share data (a Context Class containing data/state information) between other classes. The Injector (in SpecFlow's case BoDi) does 4 things for us…

1. CREATES an instance of our Context class

2. MAINTAINS that instance of our Context class during exection

3. INJECTS the context class into other classes using either…

  • i. Contructor Injection
  • ii. Parmeter Injection.

4. DISPOSES of the context class when it's no longer needed

There's a lot more to this topic and the principle of 'Inversion of Control'. You should now have an understanding of the 'dependency injection' pattern. AND know how to use this pattern with the technique called 'constructor injection'