Sharing data between step definition methods within the same class is pretty straightforward in SpecFlow – you can simply create and use a class variable – but how do we share data between steps in different classes? In the past this was done using Scenario Context and Feature Context, however these are now outdated (and don’t work when running scenarios in parallel) so from SpecFlow 3.0 we need to instead use Context Injection.
But first let’s take a step back and revisit sharing data within a single class using class instance variables, before extending that code to share data between separate classes using Context Injection.
Sharing data within a single Class
In the simple example API test below we are calling an ‘books’ endpoint with a book id to retrieve a book’s details from the back end, and we then check the response status is OK and the book id retrieved matches the id we used to request the user data.
Scenario: Check we can retrieve book data
Given a book id '1'
When the book details are retrieved
Then the response status is 'OK'
And the book id retrieved matches the requested book id
In the step definition code below for the ‘Given’ step, we store the book id value in the class variable ‘bookId’ so that it can be used later by other steps in the same class, for example in the ‘When’ step we use it to construct an endpoint URL to retrieve the book data from the API: https://localhost:3000/books/1
Also in the ‘When’ step we store the entire response data retrieved into a class instance variable ‘response’ so that it an be used by both the ‘Then’ steps to verify the book data is correct.
[Binding]
public class BookSteps
{
private string bookId;
private IRestResponse<Book> response;
[Given(@"a book id '(.*)'")]
public void GivenBookWithId(string bkid)
{
this.bookId = bkid;
}
[When(@"the book details are retrieved")]
public void WhenBookDetailsRetrieved()
{
var client = new RestClient("http://localhost:3000/");
var request = new RestRequest("books/" + this.bookId, Method.GET);
this.response = client.Execute<Book>(request);
}
[Then(@"the response status is '(.*)'")]
public void ThenResponseStatusIsCorrect(string expectedStatusCode)
{
if (expectedStatusCode == "OK")
{
Assert.AreEqual(this.response.StatusCode, HttpStatusCode.OK);
}
// ... etc
}
[Then(@"the book id retrieved matches the requested book id")]
public void ThenbookIdRetrievedMatchesRequestId()
{
Assert.AreEqual(this.response.Data.Id, this.bookId);
}
}
So that is how we can share data within the steps in a class, by using class member variables. Let’s now take a look at why we might need to share data between different classes, and how we use Context Injection to make that possible.
Also note the code is making use of a ‘Book’ class to store the retrieved data in the response:
public class Book
{
public string Id { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
}
Sharing data between different Classes using Context Injection
If we look at the steps defined in the BookSteps class above, you can see that the ‘Then’ step ThenResponseStatusIsCorrect() which verifies that the status code returned by the API call is correct, is actually a generic verification step that we would very likely want to use in other API tests and not just in our BookSteps class. So we should move this into a separate class called CommonSteps, but if we do that then the ThenResponseStatusIsCorrect() method can no longer access the ‘response’ variable because that variable’s scope is limited to the BookSteps class. We fix this problem by using Context Injection to allow sharing of data values between classes.
First let’s restructure our step definition into 2 classes, moving the ThenResponseStatusIsCorrect() method into a separate CommonSteps class:
public class BookSteps
{
private string bookId;
private IRestResponse<Book> response;
[Given(@"a book id '(.*)'")]
public void GivenBookWithId(string bkid)
{
this.bookId = bkid;
}
[When(@"the book details are retrieved")]
public void WhenBookDetailsRetrieved()
{
var client = new RestClient("http://localhost:3000/");
var request = new RestRequest("books/" + this.bookId, Method.GET);
this.response = client.Execute<Book>(request);
}
[Then(@"the book id retrieved matches the requested book id")]
public void ThenbookIdRetrievedMatchesRequestId()
{
Assert.AreEqual(this.response.Data.Id, this.bookId);
}
}
[Binding]
public class CommonSteps
{
[Then(@"the response status is '(.*)'")]
public void ThenResponseStatusIsCorrect(string expectedStatusCode)
{
if (expectedStatusCode == "OK")
{
Assert.AreEqual(this.response.StatusCode, HttpStatusCode.OK);
}
// ... etc
}
}
However this code won’t work because the ‘response’ variable cannot be shared across different classes, so we need to use Context Injection to make this happen.
To implement Context Injection and share the ‘response’ object we need to create constructors for both the BookSteps and CommonSteps classes and pass the ‘response’ object into the constructor so they can be used by the methods in those classes. However we can’t pass the existing ‘response’ object because there are a couple of problems when we try to do this:
- our current ‘response’ object is of type IRestResponse<Book> however we don’t want our CommonSteps class to receive one of these because it’s related to the Book class, and in other tests we may want to check the status code of a IRestResponse<Product> or IRestResponse<Order> responses, so we need to have a more generic approach in our CommonSteps class, and pass it a plain old IRestResponse object.
- SpecFlow doesn’t handle passing a IRestResponse<Book> class using Context Injection well and you will get a ‘interface cannot be resolved’ error when you try to do this.
The best way to implement of Context Injection in SpecFlow is to use a wrapper class for all the variables we want to share between our different classes. This is particularly useful because you can’t inject simple variables such as integers or string natively using Context Injection, so in those instances we have to use a wrapper class anyway. Let’s call our wrapper class APIContext, and in addition to the ‘response’ object let’s also share the book id as a generic ‘id’ variable.
public class APIContext
{
public IRestResponse Response { get; set; }
public string Id { get; set; }
}
Let’s now inject an APIContext variable into our BookSteps and CommonSteps classes in constructors and assign them to local instance variables:
[Binding]
public class BookSteps
{
private string bookId;
private IRestResponse<Book> response;
private APIContext context;
public BookSteps(APIContext apiContext)
{
this.context = apiContext;
}
// etc...
[Binding]
public class CommonSteps
{
private APIContext context;
public CommonSteps(APIContext apiContext)
{
this.context = apiContext;
}
// etc...
And in the BookSteps class let’s assign the response object and book id to the APIContext instance members:
[Given(@"a book id '(.*)'")]
public void GivenBookWithId(string bkid)
{
this.bookId = bkid;
this.context.Id = bkid;
}
[When(@"the book details are retrieved")]
public void WhenBookDetailsRetrieved()
{
var client = new RestClient("http://localhost:3000/");
var request = new RestRequest("books/" + this.bookId, Method.GET);
this.response = client.Execute<Book>(request);
this.context.Response = response;
}
You’ll notice we’ve kept the IRestResponse<Book> response variable so we can conveniently use response.Data as a Book type object throughout the BookSteps class code, and have also assigned it to the IRestResponse type in the APIContext class so that we can pass it as a more generic IRestResponse type into CommonSteps and other step code.
In the CommonSteps code we can then use the response object passed from the BookSteps code using the APIContext variable injected in the constructors:
[Then(@"the response status is '(.*)'")]
public void ThenResponseStatusIsCorrect(string expectedStatusCode)
{
if (expectedStatusCode == "OK")
{
Assert.AreEqual(HttpStatusCode.OK, this.context.Response.StatusCode);
}
// ... etc
}
That’s it! Now when you assign the value to the response object in the UserSteps WhenUserDetailsRetrieved() method, it will carry through to the CommonSteps code via the APIContext object.