Introduction
Last week, I announced a new series of articles dedicated to Specflow (Behavior Driven Development for .NET). In my first publication, I showed you how to create a simple test using the framework. Today’s post will be more advanced explaining the concept of SpecFlow hooks. Or how to extend the tests’ execution workflow running additional code on various points of the workflow.
What are Hooks?
Definition
The hooks (event bindings) can be used to perform additional automation logic on specific events, such
as before executing a scenario. Hooks are global but can be restricted to run only for features or
scenarios with a particular tag (see below). The execution order of hooks for the same event is
undefined.
Create SpecFlow Hooks' File
- Add new item to project.
- Select the SpecFlow's hooks' template.
The following class will be automatically generated.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
namespace ExtendTestExecutionWorkflowUsingHooks
{
[Binding]
public sealed class Hooks
{
[BeforeScenario]
public void BeforeScenario()
{
}
[AfterScenario]
public void AfterScenario()
{
}
}
}
Types of SpecFlow Hooks
SpecFlow comes with some predefined hooks that are executed after some events are fired during the tests’ execution. To make an analogy, think about TestInitialize
and TestCleanup
from MSTest framework.
The available hooks and their running order are:
[BeforeTestRun]
[BeforeFeature]
[BeforeScenario]
[BeforeScenarioBlock]
[BeforeStep]
[AfterStep]
[AfterScenarioBlock]
[AfterScenario]
[AfterFeature]
[AfterTestRun]
Attribute | Description | Is Static | MSTest Analoge |
[BeforeTestRun]
[AfterTestRun]
| Run before/after the entire test run
| Yes
| AssemblyInitialize and AssemblyCleanup
|
[BeforeFeature]
[AfterFeature]
| Run before/after executing each feature
| Yes
| ClassInitialize and ClassCleanup
|
[BeforeScenario] or [Before]
[AfterScenario] or [After]
| Run before/after executing each scenario
| No
| TestInitialize and TestCleanup
|
[BeforeScenarioBlock]
[AfterScenarioBlock]
| Run before/after executing each scenario block (e.g. between the "givens" and the "whens")
| No
| -
|
[BeforeStep]
[AfterStep]
| Run before/after executing each scenario step
| No
| -
|
SpecFlow Hooks in Tests
I will leverage on the test example from the first article from the series where we built a test for converting Kilowatt-Hours to Newton Meters. One of the drawbacks of the first implementation was that we needed to start the browser in SpecFlow background section and close it in a separate Then
step.
Previous Feature File
Feature: Convert Metrics for Nuclear Science
To do my nuclear-related job
As a Nuclear Engineer
I want to be able to convert different metrics.
Background:
Given web browser is opened
@testingFramework
Scenario: Successfully Convert Kilowatt-hours to Newton-meters
When I navigate to Metric Conversions
And navigate to Energy and power section
And navigate to Kilowatt-hours
And choose conversions to Newton-meters
And type 30 kWh
Then assert that 1.080000e+8 Nm are displayed as answer
Then close web browser
As pointed, we need to start the browser in the background section and close it in Then
step.
Bindings Class without Hooks
[Binding]
public class ConvertMetricsForNuclearScienceSteps
{
private HomePage homePage;
private KilowattHoursPage kilowattHoursPage;
[Given(@"web browser is opened")]
public void GivenWebBrowserIsOpened()
{
Driver.StartBrowser(BrowserTypes.Chrome);
}
[Then(@"close web browser")]
public void ThenCloseWebBrowser()
{
Driver.StopBrowser();
}
[When(@"I navigate to Metric Conversions")]
public void WhenINavigateToMetricConversions_()
{
this.homePage = new HomePage(Driver.Browser);
this.homePage.Open();
}
[When(@"navigate to Energy and power section")]
public void WhenNavigateToEnergyAndPowerSection()
{
this.homePage.EnergyAndPowerAnchor.Click();
}
[When(@"navigate to Kilowatt-hours")]
public void WhenNavigateToKilowatt_Hours()
{
this.homePage.KilowattHours.Click();
}
[When(@"choose conversions to Newton-meters")]
public void WhenChooseConversionsToNewton_Meters()
{
this.kilowattHoursPage = new KilowattHoursPage(Driver.Browser);
this.kilowattHoursPage.KilowatHoursToNewtonMetersAnchor.Click();
}
[When(@"type (.*) kWh")]
public void WhenTypeKWh(double kWh)
{
this.kilowattHoursPage.ConvertKilowattHoursToNewtonMeters(kWh);
}
[Then(@"assert that (.*) Nm are displayed as answer")]
public void ThenAssertThatENmAreDisplayedAsAnswer(string expectedNewtonMeters)
{
this.kilowattHoursPage.AssertFahrenheit(expectedNewtonMeters);
}
}
Here, we have binding methods for starting and closing the browser. Also, every page is created using the new
keyword.
Test Run Reuse Browser- Hooks Class
[Binding]
public sealed class TestRunSingleBrowserHooks
{
[BeforeTestRun]
public static void RegisterPages()
{
Driver.StartBrowser(BrowserTypes.Chrome);
UnityContainerFactory.GetContainer().RegisterType<HomePage>
(new ContainerControlledLifetimeManager());
UnityContainerFactory.GetContainer().RegisterType<KilowattHoursPage>
(new ContainerControlledLifetimeManager());
UnityContainerFactory.GetContainer().RegisterInstance<IWebDriver>(Driver.Browser);
}
[AfterTestRun]
public static void AfterTestRun()
{
Driver.StopBrowser();
}
}
The hooks need to be placed inside a class marked with the Binding
attribute. Here, we register all pages in the Unity IoC container and start the browser before each test run. This means that the browser will be reused accross all tests (scenarios). On AfterTestRun
, we close the browser.
Test Scenario Reuse Browser- Hooks Class
[Binding]
public sealed class TestScenarioBrowserHooks
{
[BeforeTestRun]
public static void RegisterPages()
{
UnityContainerFactory.GetContainer().RegisterType<HomePage>
(new ContainerControlledLifetimeManager());
UnityContainerFactory.GetContainer().RegisterType<KilowattHoursPage>
(new ContainerControlledLifetimeManager());
}
[BeforeScenario
public static void StartBrowser()
{
Driver.StartBrowser(BrowserTypes.Chrome);
UnityContainerFactory.GetContainer().RegisterInstance<IWebDriver>(Driver.Browser);
}
[AfterScenario]
public static void CloseBrowser()
{
Driver.StopBrowser();
}
}
If we place the code about the starting browser under BeforeScenario
method, the browser will be started for each test (scenario). Also, we need to close it in the AfterScenario
method.
Feature File with Hooks
@firefox
Feature: Convert Metrics for Nuclear Science
To do my nuclear-related job
As a Nuclear Engineer
I want to be able to convert different metrics.
@hooksExample @firefox
Scenario: Successfully Convert Kilowatt-hours to Newton-meters
When I navigate to Metric Conversions
And navigate to Energy and power section
And navigate to Kilowatt-hours
And choose conversions to Newton-meters
And type 30 kWh
Then assert that 1.080000e+8 Nm are displayed as answer
The new feature file doesn't contain any code dealing with browsers.
Bindings Class with Hooks
The class that contains steps' bindings now doesn't hold any methods that are dealing with browsers either. In the constructor, we get the pages from the Unity container instead of creating them each time with the new
keyword.
[Binding]
public class ConvertMetricsForNuclearScienceSteps
{
private readonly HomePage homePage;
private readonly KilowattHoursPage kilowattHoursPage;
public ConvertMetricsForNuclearScienceSteps()
{
this.homePage =
UnityContainerFactory.GetContainer().Resolve<HomePage>();
this.kilowattHoursPage =
UnityContainerFactory.GetContainer().Resolve<KilowattHoursPage>();
}
[When(@"I navigate to Metric Conversions")]
public void WhenINavigateToMetricConversions_()
{
this.homePage.Open();
}
[When(@"navigate to Energy and power section")]
public void WhenNavigateToEnergyAndPowerSection()
{
this.homePage.EnergyAndPowerAnchor.Click();
}
[When(@"navigate to Kilowatt-hours")]
public void WhenNavigateToKilowatt_Hours()
{
this.homePage.KilowattHours.Click();
}
[When(@"choose conversions to Newton-meters")]
public void WhenChooseConversionsToNewton_Meters()
{
this.kilowattHoursPage.KilowatHoursToNewtonMetersAnchor.Click();
}
[When(@"type (.*) kWh")]
public void WhenTypeKWh(double kWh)
{
this.kilowattHoursPage.ConvertKilowattHoursToNewtonMeters(kWh);
}
[Then(@"assert that (.*) Nm are displayed as answer")]
public void ThenAssertThatENmAreDisplayedAsAnswer(string expectedNewtonMeters)
{
this.kilowattHoursPage.AssertFahrenheit(expectedNewtonMeters);
}
}
Configure SpecFlow Hooks' Execution Order
Another cool feature of the SpecFlow hooks is that you can specific execution order if multiple hooks are specified of the same type. By default, the execution order is unspecified, and they can be executed in any order. To ensure that they are performed in a specified order, the hook attribute allows an arbitrary order to be configured. The lowest order values run before the higher order methods. After some refactoring, our hooks’ file will look like this.
[Binding]
public sealed class Hooks
{
[BeforeTestRun(Order = 1)]
public static void RegisterPages()
{
System.Console.WriteLine("Execute BeforeTestRun- RegisterPages");
Driver.StartBrowser(BrowserTypes.Chrome);
UnityContainerFactory.GetContainer().RegisterType<HomePage>
(new ContainerControlledLifetimeManager());
UnityContainerFactory.GetContainer().RegisterType<KilowattHoursPage>
(new ContainerControlledLifetimeManager());
}
[BeforeTestRun(Order = 2)]
public static void RegisterDriver()
{
System.Console.WriteLine("Execute BeforeTestRun- RegisterDriver");
UnityContainerFactory.GetContainer().RegisterInstance<IWebDriver>(Driver.Browser);
}
[AfterTestRun]
public static void AfterTestRun()
{
System.Console.WriteLine("Execute AfterTestRun- StopBrowser");
Driver.StopBrowser();
}
[BeforeFeature]
public static void BeforeFeature()
{
}
[AfterFeature]
public static void AfterFeature()
{
}
[BeforeScenario(Order = 2)]
public static void StartBrowser()
{
System.Console.WriteLine("Execute BeforeScenario- StartBrowser");
}
[BeforeScenario(Order = 1)]
public static void LoginUser()
{
System.Console.WriteLine("Execute BeforeScenario- LoginUser");
}
[AfterScenario(Order = 2)]
public static void CloseBrowser()
{
System.Console.WriteLine("Execute AfterScenario- CloseBrowser");
}
[AfterScenario(Order = 1)]
public static void LogoutUser()
{
System.Console.WriteLine("Execute AfterScenario- LogoutUser");
}
[BeforeStep]
public void BeforeStep()
{
System.Console.WriteLine("BeforeStep- Start Timer");
}
[AfterStep]
public static void AfterStep()
{
System.Console.WriteLine("BeforeStep- Log something in DB.");
}
}
Hooks' Tag Scoping
We can scope based on tags. The tags are added to each test scenario starting with the ‘@
’ symbol. Most of the hooks support tag scoping, meaning that they are executed only if the feature or the scenario has at least one of the tags specified in the filter.
Scope Attribute
You can use the new Scope
attribute to specify the tag.
[AfterScenario(Order = 1)]
[Scope(Tag = "hooksExample")]
public static void LogoutUser()
{
System.Console.WriteLine("Execute AfterScenario- LogoutUser");
}
Step Attribute
Also, you can specify the tag scoping in the steps' attribute constructor.
[AfterScenario(Order = 1)]
[AfterScenario("hooksExample")]
public static void LogoutUser()
{
System.Console.WriteLine("Execute AfterScenario- LogoutUser");
}
Advanced Tag Scoping
If you use the ScenarioContext
class, you can perform even more advanced scoping. In the below example, we throw an exception if the browser tag is not specified.
[BeforeScenario(Order = 2)]
public static void StartBrowser()
{
if (!ScenarioContext.Current.ScenarioInfo.Tags.Contains("firefox"))
{
throw new ArgumentException("The browser is not specfied");
}
System.Console.WriteLine("Execute BeforeScenario- StartBrowser");
}
Hooks' Methods Execution Order
[Binding]
public sealed class OrderHooks
{
[BeforeTestRun(Order = 1)]
public static void RegisterPages()
{
System.Console.WriteLine("BeforeTestRun");
Driver.StartBrowser(BrowserTypes.Chrome);
UnityContainerFactory.GetContainer().RegisterType<HomePage>
(new ContainerControlledLifetimeManager());
UnityContainerFactory.GetContainer().RegisterType<KilowattHoursPage>
(new ContainerControlledLifetimeManager());
}
[BeforeTestRun(Order = 2)]
public static void RegisterDriver()
{
System.Console.WriteLine("Execute BeforeTestRun- RegisterDriver");
UnityContainerFactory.GetContainer().RegisterInstance<IWebDriver>(Driver.Browser);
}
[AfterTestRun]
public static void AfterTestRun()
{
System.Console.WriteLine("AfterTestRun");
Driver.StopBrowser();
}
[BeforeFeature]
public static void BeforeFeature()
{
System.Console.WriteLine("BeforeFeature");
}
[AfterFeature]
public static void AfterFeature()
{
System.Console.WriteLine("AfterFeature");
}
[BeforeScenario]
public void LoginUser()
{
System.Console.WriteLine("BeforeScenario");
}
[AfterScenario(Order = 1)]
public void AfterScenario()
{
System.Console.WriteLine("AfterScenario");
}
[BeforeStep]
public void BeforeStep()
{
System.Console.WriteLine("BeforeStep");
}
[AfterStep]
public void AfterStep()
{
System.Console.WriteLine("AfterStep");
}
}
Actually, the after test is executed, I am not sure why it was not printed in the output. Anyway, it is executed last.
Specflow Series
The post Advanced SpecFlow: Using Hooks to Extend Test Execution Workflow appeared first on Automate The Planet.
All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement