Advanced Specflow Tutorial on Shared & Scoped Bindings, Hooks and Step Reuse:
In this Complete Guide on Specflow Training, we had a look at End to End Example of Using Specflow in detail in our previous tutorial.
In our previous article, we saw, an example to share data using private instance variables of the binding class and then referred to the same instance across different steps.
Often at times, it is required that the code for a lot of step implementation (especially setup) can be shared across different scenarios, as well as the actual data that’s being used in different steps for performing different actions.
Table of Contents:
Here are the video tutorials on Shared & Scoped Bindings, Hooks and Step Reuse
VIDEO #1: Specflow Context Sharing
VIDEO #2: Specflow Hooks and Scoped Bindings
This approach has a lot of shortcomings like:
- How would you share the data across different binding files?
- It can be done by creating copies of instances/variables but it will end up muddying the code everywhere and will cause inconsistencies.
Specflow provides 2 alternative approaches to handle this kind of scenarios:
- Keeping context data in ScenarioContext object.
- Sharing data with the lightweight dependency/context injection framework that specflow provides.
Multiple Binding files
Let us first understand the use case of multiple binding/step implementation files. Generally in all projects, when you decide to use Specflow, you will be defining multiple features and grouping the binding implementation as per your needs.
For Example You may decide to keep all the bindings related to setup. Example, opening web page, navigating to menus etc. in one binding file, all the search input related definitions in another binding file and so on.
Hence, multiple binding files will essentially be a part of any project where Specflow is being used as the testing tool/framework. It also adds to a good design practice of Separation of Concerns which is a key pillar of the SOLID design framework.
Now in order to share the context across these binding files, we will look at the possible approaches in the upcoming sections of this tutorial and refer the same youtube search example (that was discussed/implemented in the last article) for all the approaches that we will discuss.
Dependency Injection
Let us first try to understand what Dependency Injection is?
Dependency Injection is a common term and practice with a lot of projects and in simple terms, it means to introduce/inject all the dependencies required by the code at run time. This can be done using Constructor Parameters when the Object of the class is getting initialized.
There are a lot of open source frameworks that handle dependency injections like AutoFac for .NET, Google Guice for JAVA etc.
Since, the intention here is not to go deep into what’s and how’s of dependency injection, let’s aim to understand from Specflow’s point of view which already provides a lightweight context injection framework.
Steps
Given below are the sequence of steps that need to be followed, in order to inject dependencies/context in binding files:
- Create Classes (POCOs) for the context object/data that needs to be shared.
- For each of the binding classes, create a local instance of Context class type and instantiate it using the Constructor Parameter.
- Now whatever changes are done to the context parameter will get passed to the next class where the object will be initialized using the Constructor Parameter.
Example – Context Injection
Let’s look at this Context Injection in action for our Youtube Search Example.
We had instance variables for 2 things/data items:
- For storing the search term as it was used across multiple steps.
- For storing the actual webdriver instance so that same instance can be reused/referred to across multiple steps.
Now, using Context Injection, we will be creating 2 POCOs i.e. 1 each for storing the search- related data and we will call it as SearchContext.cs and the other one for storing all the info related to the actual webdriver instance and we call it as – WebDriverContext.cs
SearchContext.cs file will have a String variable to store the search term.
WebDriverContext.cs will contain an instance of ChromeDriver which will drive the entire test.
Let’s see below how both of these files look. Both of these are simple Plain Old C# files, which have instance fields to store the actual context information.
Both these files are added in a new namespace called Context to have a logical separation from Features and Bindings.
public class SearchContext { public SearchContext() { this.searchTerm = String.Empty; } public String searchTerm; } public class WebDriverContext { public WebDriverContext() { this.chromeDriver = new ChromeDriver(); } public ChromeDriver chromeDriver; }
Now for illustration purposes, let’s split our Binding file into 2 files i.e. Just for illustration/explanation purpose, We will be moving the Validation (or all scenario steps for Then group i.e. Validation) and we will be calling it as “YoutubeSearchVerificationSteps.cs”
So, we now have 2 binding files:
- YoutubeSearchFeatureSteps.cs
- YoutubeSearchVerificationSteps.cs
The breakup of steps is shown below:
Scenario: Youtube should search for the given keyword and should navigate to search results page Given I have navigated to youtube website //YoutubeSearchFeatureSteps.cs And I have entered India as search keyword //YoutubeSearchFeatureSteps.cs When I press the search button //YoutubeSearchFeatureSteps.cs Then I should be navigate to search results page //YoutubeSearchFeatureVerificationSteps.cs
Once we have the context file/objects ready, then we are now in a position to utilize and initialize the context values as the scenario execution progresses.
Let’s first see, how and where the actual injection is taking place. For the individual binding files, we will be injecting the Context instances via Constructor as shown below.
The changes/updates to the instances done in one binding file will be injected in the other binding files as and when they get initialized.
private readonly SearchContext _searchContext; private readonly WebDriverContext _webDriverContext; public YoutubeSearchFeatureSteps(SearchContext searchContext, WebDriverContext webDriverContext) { _searchContext = searchContext; _webDriverContext = webDriverContext; }
The above code injects the objects of SearchContext and WebDriverContext through the constructor. The code remains the same in all binding files wherever the context needs to be initialized, just the name of the Constructor changes as per the Binding File Class Name.
Let’s try to go over each step of the Scenario and understand where we are updating our context variables.
Step 1
Given I have navigated to youtube website //YoutubeSearchFeatureSteps.cs
This step will update the properties of webDriver like URL, pageTitle etc.
Step 2
And I have entered India as search keyword //YoutubeSearchFeatureSteps.cs
Here, in addition to updation of the state of webDriver, we will also update SearchContext’s -> searchTerm field and set it to the actual search keyword i.e. “India” from the Scenario Step.
Step 3
When I press the search button //YoutubeSearchFeatureSteps.cs
This will just result in further updation of the state of WebDriver instance.
Till this point, all these 3 steps belong to the same binding file and we have updated the stats of the Context objects that were injected.
Step 4
Then I should be navigate to search results page //YoutubeSearchFeatureVerificationSteps.cs
This step is a part of another Binding file, where also we have injected the context in the same way through Constructor as we did for the first binding file.
In the actual Step definition code, when you try to access the state of WebDriverContext’s chromeDriver or the values of instance members of SearchContext, you will see that the values that were set in the previous binding file have been propagated to the injected instance and can be accessed in the step implementation code anywhere in the binding file.
Now, let’s try executing this Scenario end to end by clicking the Run Scenario option from the Test Explorer. Now you will notice that the test passes and it can access all values of injected context classes across the binding files wherever it is injected through the respective Constructors.
There are a lots of advantages of having context injection in place and some of them are listed below:
- Separation of concerns – test context is different from the actual test code.
- Reuse of context across bindings.
- Leads to better modular design.
Using Scenario Context & Feature Context
ScenarioContext and FeatureContext are static classes that are capable of holding the shared state/context during the execution of a scenario and feature respectively.
Think of these 2 classes as a key-value dictionary where the key being name of the state variable and value is the actual object which might be as simple as a primitive int or String and as complex as a user-defined custom Class.
An important point to note here is when the value of a key is retrieved, it always returns an Object that is required to be casted into a specific type as desired.
Syntax:
Setting a value in ScenarioContext:
ScenarioContext.Current[“key“] = value;
Getting a value from ScenarioContext: Depending upon the type of the value (the type of object essentially) that was set in ScenarioContext, while fetching the value, it needs to be cast to the respective type. Suppose the type of Object that was set was of type int then in order to get the value, we will need to do an explicit cast to int.
var value = (int)ScenarioContext.Current[“key“]
Please note, that the scope of ScenarioContext variables is for scenario execution.
Similar to ScenarioContext, there exists another static class called FeatureContext. The only difference here is that the scope of data for FeatureContext is until the entire Feature execution is complete.This is generally less rarely used as usually there is a lesser need to keep context/data alive for the entire feature as opposed to per scenario.
So, now let us write/implement the same piece of code using ScenarioContext instead of the Specflow’s context injection framework.
To illustrate the usage of ScenarioContext, we will be using it to hold states of the search term in our example i.e. wherever the search term is first used, at that time, we will set the value in ScenarioContext and for all future usages, we will refer to the value fetched from the ScenarioContext.
So, essentially we will be making changes in our Act (When) step where we perform a search for the given search term and then we will fetch the value from ScenarioContext in our Assertion Step (Then) where we validate that the results are for a specified search term.
Look at the implementation of Step definitions below with ScenarioContext:
[Given(@"I have entered (.*) as search keyword")] public void GivenIHaveEnteredIndiaAsSearchKeyword(String searchString) { ScenarioContext.Current["searchTerm"] = searchString.ToLower(); var searchInputBox = _webDriverContext.chromeDriver.FindElementById("search"); var wait = new WebDriverWait(_webDriverContext.chromeDriver, TimeSpan.FromSeconds(2)); wait.Until(ExpectedConditions.ElementIsVisible(By.Id("search"))); searchInputBox.SendKeys(searchString); }
[Then(@"I should be navigate to search results page")] public void ThenIShouldBeNavigateToSearchResultsPage() { var expectedSearchTerm = (String)ScenarioContext.Current["searchTerm"]; System.Threading.Thread.Sleep(2000); // After search is complete the keyword should be present in url as well as page title` Assert.IsTrue(_webDriverContext.chromeDriver.Url.ToLower().Contains(expectedSearchTerm)); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains(expectedSearchTerm)); }
As you can see above, in the Assertion step, we have fetched the value of context using the stored key and used it for validating the output with the supplied input.
In the next section, we will discuss the special events provided by Specflow called Hooks which can be utilized to execute setup and cleanup code at various stages of test execution.
Hooks in Specflow
Hooks are special events that are raised by the Specflow framework while it is executing a feature and a scenario. These events when generated, provide an opportunity to write an event handler and any code that you want to associate with the specific event.
For Example, let’s look at “BeforeScenario” hook and from the name itself it is evident, that this event will be raised before running any scenario from the feature. Think of it as test initialize setup in other unit testing frameworks like MSUnit (for C#) and Junit (for Java)
First, let’s see, how the hooks are added as part of the tests. Though hooks can be added to any file which has Binding attribute, Specflow also provides a separate type of class file of type hooks (which also has a Binding attribute attached).
It’s generally a good idea to keep the hooks in a separate file in order to have more intuitive logical grouping.
As shown in the below figure, add a specflow Hooks file to the Step Definitions folder (and name it as YoutubeSearchHooks.cs)
Let’s see what all hooks are provided by Specflow along with examples (for our Youtube Search example) in the below section (To keep it simple, We will be looking at them in the form of pairs):
#1) BeforeTestRun, AfterTestRun: This is the top level hook and it allows the execution of code before a test run starts and after the test run completes. Please note that this is above the Feature level as well.
Hence, if your code has multiple feature files – then the code mentioned in these hooks will run once before the test run starts and once after the code execution completes.
There are a few important points that should be noted here:
- Please note that when tests are run with multiple threads then both of these hooks are executed once for each of the threads.
- The bindings for these hooks should be static as the test context has still not been set before the test (and is not available after the test).
Simply add 2 static methods in the binding file that we created and add attributes [BeforeTestRun] and [AfterTestRun] respectively.
For simplicity, we have just written console statements (but these hooks can contain any initialization and cleanup logic as required)
[BeforeTestRun] public static void BeforeTestRun() { Console.WriteLine("In Method Before test run!!"); } [AfterTestRun] public static void AfterTestRun() { Console.WriteLine("In Method After test run!!"); }
#2) BeforeFeature, AfterFeature: As the name implies, these hooks will execute once for each feature files before and after completion respectively. (If there is just one feature file then the hooks will execute just once). Like Before and After the test run, the methods that contain the code for these hooks should be static.
Add these as 2 static methods in the binding file that was created for hooks.
[BeforeFeature] public static void BeforeFeature() { Console.WriteLine("In Method Before Feature!"); } [AfterFeature] public static void AfterFeature() { Console.WriteLine("In Method After Feature!"); }
#3) BeforeScenarioBlock, AfterScenarioBlock: These hooks run before every type of scenario block i.e. before every group of “Given”, “When” & “Then”
Unlike hooks for test and feature level, the binding methods for these hooks need not be static.
Let’s look at the code Example below:
[BeforeScenarioBlock] public void BeforeScenarioBLock() { Console.WriteLine("In Method Before Scenario block!"); } [AfterScenarioBlock] public void AfterScenarioBlock() { Console.WriteLine("In Method After ScenarioBlock!"); }
If you execute the same scenario, with the above hooks, you can see the console statements getting printed in the test output before and after each scenario block. Refer to the below screenshot, with highlighted Scenario blocks and console output.
#4) BeforeScenario, AfterScenario: Again as the name implies, these hooks will be executed once per each scenario, before it starts and after it completes. These are the most commonly used hooks as they are the perfect place to have setup and cleanup logic respectively.
Simply add method bindings with attributes BeforeScenario and AfterScenario for implementing these hooks in the code.
[BeforeScenario] public void BeforeScenario() { Console.WriteLine("In Method Before Scenario!"); } [AfterScenario] public void AfterScenario() { Console.WriteLine("In Method After Scenario!"); }
Let’s look at the output for scenario execution (hook output shown as highlighted below)
#5) BeforeStep, AfterStep: Both these hooks are per step level i.e. these execute for each individual step of the scenario.
To implement, add method bindings in the hooks file with attributes BeforeStep and AfterStep respectively.
[BeforeStep] public void BeforeStep() { Console.WriteLine("In Method Before Step!"); } [AfterStep] public void AfterStep() { Console.WriteLine("In Method After Step!"); }
Hence, if we have 4 steps in a scenario, these hooks will execute once for each step.
Let’s see, how the output looks like for these hooks (highlighted below):
Scoped Bindings
Scoping is one of the most important feature of specflow and it allows you to add a lot of flexibility to your test organization and execution by using tags for the tests, and restrict bindings to be applied to a Feature or Scenario or Feature/Scenario having some tags.
Think of it like executing/defining bindings depending on the scope defined, like a feature/scenario title or a tag (this overrides the global nature of binding and hides some bindings by restricting to a more confined and limited scope)
This comes in handy in a lot of situations like:
- The text of the scenario step is the same, but you may want different treatment for it depending on the scenario that is getting executing.
- You don’t want to reuse/share a binding implementation across features/scenarios
Let’s understand the different types of scoping techniques and apply them on the same youtube search feature example.
We will be copying the same feature file and will be just changing the name of the feature to illustrate the Scoping rules in a better way.
Hence after creating a copy of the feature, our code structure will look as shown below:
Scoping by Tag
Scoping by tag, restricts the binding files by tag values on feature or scenario.
Let’s first try to understand what is a tag in Specflow and where all it can be applied?
A tag in Specflow is nothing but a way to categorize features and scenarios in a group (essentially logical grouping) and can help in using features like binding scopes, running scenarios/features with a particular tag etc.
Let’s apply a tag “test” on Scenario in any one of the feature files.
@test Scenario: Youtube should search for the given keyword and should navigate to search results page Given I have navigated to youtube website And I have entered India as search keyword When I press the search button Then I should be navigate to search results page
Now, let’s add tag scope to any one of the step definitions i.e. let’s say for the first step like this:
[Scope(Tag ="test")] [Given(@"I have navigated to youtube website")] public void GivenIHaveNavigatedToYoutubeWebsite() { _webDriverContext.chromeDriver.Navigate().GoToUrl("https://www.youtube.com"); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains("youtube")); }
Once the above binding is scoped, it will be executed only for those scenario/features, that will have this tag available.
Also, please note that this tag can not only be placed around a Scenario but, also at Feature level. Let’s apply this tag to the second feature file (which is actually a copy of the first one).
@test Feature: YoutubeSearchFeature2 In order to test search functionality on youtube As a developer I want to ensure functionality is working end to end
You’ll see that as soon as this tag is added to feature, the bindings which became hidden for the scenario in this feature file will again start getting executed.
Scoping by Scenario Title
Similar to the approach for Scoped bindings with tag, Scenario title can also be used for defining the Scope of a binding method.
[[Scope(Scenario = "Youtube should search for the given keyword and should navigate to search results page")] [Given(@"I have navigated to youtube website")] public void GivenIHaveNavigatedToYoutubeWebsite() { _webDriverContext.chromeDriver.Navigate().GoToUrl("https://www.youtube.com"); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains("youtube")); }
Scoping by Feature Title
Similar to Scenario-based scoping, you can apply Feature-based scoping by just mentioning the Feature title in Scope.
[Scope(Feature= "YoutubeSearchFeature")] [Given(@"I have navigated to youtube website")] public void GivenIHaveNavigatedToYoutubeWebsite() { _webDriverContext.chromeDriver.Navigate().GoToUrl("https://www.youtube.com"); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains("youtube")); }
Scoping – Tips and Tricks
#1) It is possible to use a combination of scoping rules as well. Like a combination of tag and ScenarioTitle, so suppose there is a tag at feature level and you want to restrict a binding scope to a specific scenario in that Feature file then you could use a combination of Tag and ScenarioTitle.
[Scope(Tag=”test” Feature= "YoutubeSearchFeature")] [Given(@"I have navigated to youtube website")]
#2) Scoping rules can also be defined at the class level itself along with the Binding attribute. This will result in all binding implementations specified in that class to follow the Scoped restrictions.
[Binding, Scope(Tag ="test")] public class YoutubeSearchFeatureSteps : IDisposable
#3) Scoping can be applied on hooks as well. This comes in handy when you want to have for instance different initialization logic based on tags available on scenarios. Had this been not the case then you would have to write all initialization logic as a part of the individual steps as well.
Let’s see an Example below:
[BeforeStep("test")] public void BeforeStep() { Console.WriteLine("In Method Before Step!"); }
Other Specflow Keywords
Background
Specflow provides a special keyword termed as “Background” which is kind of Scenario setup in a feature file, in the way we have hooks for test setup, and its similar to Scenario setup. With “Background” you can define initial data/steps that are required by all the scenarios of the file and it essentially helps to avoid duplicating the steps in each and every scenario in the feature file.
In order to illustrate this, let’s add a new scenario in our file which searches for another keyword say “America”. Now both the scenarios have something in common. Like both the scenarios have a pre-requisite to be on the Youtube site. We can add this pre-requisite step in the “Background” section too.
Look at the code Example below:
Background: Given I have navigated to youtube website Scenario: Youtube keyword search 1 And I have entered India as search keyword When I press the search button Then I should be navigate to search results page Scenario: Youtube keyword search 2 And I have entered America as search keyword When I press the search button Then I should be navigate to search results page
So, with the above example, you can see that we have added one step in the Scenario Background. Now whenever any scenario from this feature file is run, the background step will execute before each scenario.
Few points to note:
- A feature file can have only one Background section.
- The Background section should be defined before the first scenario of the feature file.
ScenarioOutline & Examples
ScenarioOutline is another useful keyword from Specflow which is used for supporting data-driven tests and is a quite common practice that is especially for unit tests, where we try to test the same function with different inputs.
It also helps to avoid duplication of scenario steps for each different data sample that needs to be verified.
Let’s use the same example as above for youtube and create a scenario outline for searching 2 different input search terms – India and America. This is a perfect example of data driven test where just the search parameter changes and the rest all remains the same.
Also, ScenarioOutline can be combined with other keywords like Background to make it even more powerful.
Few important points to note:
- The data values for which scenario needs to be run are placed in the “Examples” section at the end of the scenario.
- The first row of the Examples section acts as column/variable names that are referred to in the actual step and replaced with the actual input value when the scenario gets executed.
Let’s look at the Example below:
Background: Given I have navigated to youtube website Scenario Outline: Youtube keyword search And I have entered <searchTerm> as search keyword When I press the search button Then I should be navigate to search results page Examples: | searchTerm | | India | |America |
Conclusion
With the out of the box context injection framework of Specflow, it makes it really simple to inject context and reuse across different binding files. The DI framework that Specflow internally uses is BoDI
The other approaches to reuse object/data across Scenario is Specflow hooks for ScenarioContext (and FeatureContext if you want some data to be retained across the entire feature execution).
Both of these approaches provide separation of concern and allow the context to be reused across bindings in contrast to keeping local instance variables in each and every Binding file.
We also looked at other Specflow features like
- Hooks – which allow special logic to be placed during different events that take place during test execution like BeforeScenario/AfterScenario, BeforeFeature/AfterFeature etc.
- Specflow keywords like Background and ScenarioOutline help to avoid duplicating the scenarios and keep the feature files crisp and simple.
Code Files
Feature Files
#1) YoutubeSearchFeature.feature
Feature: YoutubeSearchFeature In order to test search functionality on youtube As a developer I want to ensure functionality is working end to end @test Scenario: Youtube should search for the given keyword and should navigate to search results page Given I have navigated to youtube website And I have entered India as search keyword When I press the search button Then I should be navigate to search results page
Step Definition files
#1) YoutubeSearchFeatureSteps.cs
[Binding] public class YoutubeSearchFeatureSteps : IDisposable { private readonly SearchContext _searchContext; private readonly WebDriverContext _webDriverContext; public YoutubeSearchFeatureSteps(SearchContext searchContext, WebDriverContext webDriverContext) { _searchContext = searchContext; _webDriverContext = webDriverContext; } [Given(@"I have navigated to youtube website")] public void GivenIHaveNavigatedToYoutubeWebsite() { _webDriverContext.chromeDriver.Navigate().GoToUrl("https://www.youtube.com"); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains("youtube")); } [Given(@"I have entered (.*) as search keyword")] public void GivenIHaveEnteredIndiaAsSearchKeyword(String searchString) { _searchContext.searchTerm = searchString.ToLower(); var searchInputBox = _webDriverContext.chromeDriver.FindElementById("search"); var wait = new WebDriverWait(_webDriverContext.chromeDriver, TimeSpan.FromSeconds(2)); wait.Until(ExpectedConditions.ElementIsVisible(By.Id("search"))); searchInputBox.SendKeys(_searchContext.searchTerm); } [When(@"I press the search button")] public void WhenIPressTheSearchButton() { var searchButton = _webDriverContext.chromeDriver.FindElementByCssSelector("button#search-icon-legacy"); searchButton.Click(); } public void Dispose() { if (_webDriverContext.chromeDriver != null) { _webDriverContext.chromeDriver.Dispose(); _webDriverContext.chromeDriver = null; } } }
#2) YoutubeSearchFeatureVerificationSteps.cs
[Binding] public sealed class YoutubeSearchFeatureVerificationSteps { private readonly SearchContext _searchContext; private readonly WebDriverContext _webDriverContext; public YoutubeSearchFeatureVerificationSteps(SearchContext searchContext, WebDriverContext webDriverContext) { _searchContext = searchContext; _webDriverContext = webDriverContext; } [Then(@"I should be navigate to search results page")] public void ThenIShouldBeNavigateToSearchResultsPage() { System.Threading.Thread.Sleep(2000); // After search is complete the keyword should be present in url as well as page title` Assert.IsTrue(_webDriverContext.chromeDriver.Url.ToLower().Contains(_searchContext.searchTerm)); Assert.IsTrue(_webDriverContext.chromeDriver.Title.ToLower().Contains(_searchContext.searchTerm)); } [AfterScenario] public void AfterScenarioCompletion() { _webDriverContext.chromeDriver.Close(); } }
#3) YoutubeSearchHooks.cs
[Binding] public sealed class YoutubeSearchHooks { [BeforeScenario] public void BeforeScenario() { Console.WriteLine("In Method Before Scenario!"); } [AfterScenario] public void AfterScenario() { Console.WriteLine("In Method After Scenario!"); } [BeforeStep("test")] public void BeforeStep() { Console.WriteLine("In Method Before Step!"); } [AfterStep] public void AfterStep() { Console.WriteLine("In Method After Step!"); } [BeforeScenarioBlock] public void BeforeScenarioBLock() { Console.WriteLine("In Method Before Scenario block!"); } [AfterScenarioBlock] public void AfterScenarioBlock() { Console.WriteLine("In Method After ScenarioBlock!"); } [BeforeFeature] public static void BeforeFeature() { Console.WriteLine("In Method Before Feature!"); } [AfterFeature] public static void AfterFeature() { Console.WriteLine("In Method After Feature!"); } [BeforeTestRun] public static void BeforeTestRun() { Console.WriteLine("In Method Before test run!!"); } [AfterTestRun] public static void AfterTestRun() { Console.WriteLine("In Method After test run!!"); } }
Our upcoming tutorial will brief you on Step Argument Transformations & Specflow tables in detail!