The short version
I have a SpecFlow scenario outline which uses Selenium to test a web page. The scenario outline has many rows of Examples
test data and for performance reasons I would like to open one browser window and run every test in that window, rather than opening a new browser window for every row in the test data.
My minimal reproducible example
Application being tested
This is a basic web page which allows the user to input two numbers, multiplies them and displays the result.
<!DOCTYPE html>
<html>
<body>
<!-- Multiply this number... -->
<input type="text" id="operand1" />
*
<!-- ...by this number... -->
<input type="text" id="operand2" />
=
<!-- ...and the result should be displayed here... -->
<span id="product"></span>
<div>
<!-- ...when I click this button -->
<input type="button" id="calculate" value="Calculate" onclick="calculate()" />
</div>
<!-- import jQuery so that I can be confident about browser compatibility -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"
integrity="sha512-aVKKRRi/Q/YV 4mjoKBsE4x3H BkegoM/em46NNlCqNTmUYADjBbeNefNxYV7giUp0VxICtqdrbqU7iVaeZNXA=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<!-- the script which does the multiplication -->
<script>
function calculate() {
var operand1 = $('#operand1').val();
var operand2 = $('#operand2').val();
var product = operand1 * operand2;
$('#product').text(product);
}
</script>
</body>
</html>
Feature file
Feature: Multiplication
As someone who's no good at mental arithmetic
I want something to multiply one number by another number
So that I can know the answer without working it out myself
Background:
Given I am on the multiplication page
Scenario Outline: Multiply two numbers
Given the first number is <operand1>
And the second number is <operand2>
When I click the calculate button
Then the result should be <product>
Examples:
| operand1 | operand2 | product |
| 1 | 2 | 2 |
| 2 | 2 | 4 |
| 3 | 2 | 6 |
| 4 | 2 | 8 |
# Rather than make you scroll through hundreds of lines of test data,
# I'd like you to imagine that there are hundreds of lines of test
# data here. What the data is doesn't really matter.
Step definitions
using OpenQA.Selenium;
[Binding]
public sealed class MultiplicationSteps : Steps
{
private readonly IWebDriver driver;
public MultiplicationSteps(ScenarioContext context)
{
this.driver = (IWebDriver)context["driver"];
}
[Given(@"I am on the multiplication page")]
public void GivenIAmOnTheMultiplicationPage()
{
this.driver.Navigate().GoToUrl(@"file:///C:/path/to/multiplier.html");
}
[Given("the first number is (.*)")]
public void GivenTheFirstNumberIs(int number)
{
var operand1Box = this.driver.FindElement(By.Id("operand1"));
operand1Box.Clear();
operand1Box.SendKeys(number.ToString());
}
[Given("the second number is (.*)")]
public void GivenTheSecondNumberIs(int number)
{
var operand2Box = this.driver.FindElement(By.Id("operand2"));
operand2Box.Clear();
operand2Box.SendKeys(number.ToString());
}
[When("I click the calculate button")]
public void WhenIClickTheCalculateButton()
{
var calculateButton = this.driver.FindElement(By.Id("calculate"));
calculateButton.Click();
}
[Then("the result should be (.*)")]
public void ThenTheResultShouldBe(int expectedProduct)
{
var productDiv = this.driver.FindElement(By.Id("product"));
var product = productDiv.Text;
Assert.Equal(expectedProduct.ToString(), product);
}
}
Hooks
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
[Binding]
public class Hooks : Steps
{
[BeforeScenario]
public void BeforeScenario()
{
if (!this.ScenarioContext.ContainsKey("driver"))
{
this.ScenarioContext["driver"] = new ChromeDriver();
}
}
[AfterScenario]
public void AfterScenario()
{
var driver = this.ScenarioContext["driver"] as IWebDriver;
if (driver != null)
{
driver.Quit();
driver.Dispose();
}
}
}
What's wrong?
Functionally this is fine, the tests all pass, but the performance sucks because it's opening a new browser window for every row in the Examples
test data, because [BeforeScenario]
runs once per row in the Examples
rather than once for the whole Scenario Outline
. Opening and closing all those browser windows adds an awful lot to the total execution time.
What I've tried
BeforeFeature / AfterFeature
It feels like I need a [BeforeScenarioOutline]
, but that's not a thing. The next level up is [BeforeFeature]
, so I tried storing the IWebDriver
in the FeatureContext
instead of the ScenarioContext
like this:
[BeforeFeature]
public void BeforeFeature()
{
this.FeatureContext["driver"] = new ChromeDriver();
}
[AfterFeature]
public void AfterFeature()
{
var driver = this.FeatureContext["driver"] as IWebDriver;
if (driver != null)
{
driver.Quit();
driver.Dispose();
}
}
But this results in a build warning and runtime error
The binding methods for before/after feature and before/after test run events must be static!
And of course if I make those methods static
then they can't access the instance property this.FeatureContext
. The same is true of BeforeTestRun
and AfterTestRun
.
Static class to hold the IWebDriver
This class will instantiate the IWebDriver at some point before I want to use it:
public static class TheDriver
{
private static readonly IWebDriver driver = new ChromeDriver();
public static IWebDriver Driver => driver;
}
And I changed the step definitions constructor to this:
public MultiplicationSteps()
{
this.driver = TheDriver.Driver;
}
And now I can make use of [AfterTestRun]
to close the browser at the end of the test run.
[AfterTestRun]
public static void AfterTestRun()
{
if (TheDriver.Driver != null)
{
TheDriver.Driver.Quit();
TheDriver.Driver.Dispose();
}
}
This works as long as I only have one feature file. But it's not thread safe - as soon as I try running two features in parallel the tests fail because both features are trying to work with the same browser window.
Summary
I feel like I'm missing something important about how to use BeforeFeature
, AfterFeature
and FeatureContext
correctly. Unfortunately the SpecFlow hooks documentation doesn't say a great deal about how to go about this.
My tech stack
- .net 6.0
- Selenium.WebDriver 4.4.0
- SpecFlow 3.9.40
- xunit 2.4.1
CodePudding user response:
Using Hooks with Parameter Injection in the SpecFlow documentation describes how to access the FeatureContext from a [BeforeFeature]
hook. It is deceptively simply, actually. Declare a FeatureContext object as a parameter to your BeforeFeature hook:
[BeforeFeature]
public static void CreateWebDriver(FeatureContext feature)
{ // ^^^^^^^^^^^^^^^^^^^^^^
feature["driver"] = new ChromeDriver();
}
[AfterFeature]
public static void DestroyWebDriver(FeatureContext feature)
{ // ^^^^^^^^^^^^^^^^^^^^^^
var driver = (IWebDriver)feature["driver"];
driver.Dispose();
}