Home > Mobile >  How do I run every example in a scenario outline in a single browser session?
How do I run every example in a scenario outline in a single browser session?

Time:09-02

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();
}
  • Related