Targeting Multiple Browser with a Single Test

If you are testing a web app (e.g. with Selenium), you will normally want to test it in a range of browsers, e.g. Chrome, IE/Edge and Firefox. However, writing tests for all the browsers can be a time-consuming process. Wouldn’t it be much easier to just write just one test, and be able to run that test in all browsers?

That’s where using targets with the SpecFlow+ Runner comes in. Targets are defined in your SpecFlow+ Runner profile. They allow you to define different environment settings, filters and deployment transformation steps for each target. Another common use case is to define separate targets for X64 and x86.

Defining targets for each browser allows us to execute the same test in all browsers. You can see this in action in the Selenium sample project available on GitHub. If you download the solution and open Default.srprofile, you will see 3 different targets defined at the end of the file:

<Targets>
  <Target name="IE">
    <Filter>Browser_IE</Filter>
  </Target>
  <Target name="Chrome">
    <Filter>Browser_Chrome</Filter>      
  </Target>
  <Target name="Firefox">
    <Filter>Browser_Firefox</Filter>
  </Target>
</Targets>

Each of the targets has a name and an associated filter (e.g. “Browser_IE”). The filter ensures that only tests with the corresponding tag are executed for that target.

For each target, we are going to transform the browser key in the appSettings section of our app.config file to contain the name of the target. This will allow us to know the current target and access the corresponding web driver for each browser. The corresponding section in the project’s app.config file is as follows:

  <appSettings>
    <add key="seleniumBaseUrl" value="http://localhost:58909" />
    <add key="browser" value="" />
</appSettings>

To make sure that the name of the target/browser is entered, we transform the app.config file to set the browser key’s value attribute to the name of the target using the {Target} placeholder. This placeholder is replaced by the name of the current target during the transformation:

<Transformation>
  <![CDATA[<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
      <appSettings>
        <add key="browser" value="{Target}"
        xdt:Locator="Match(key)"
        xdt:Transform="SetAttributes(value)" />
      </appSettings>
    </configuration>
  ]]>
</Transformation>

This section locates the key (browser) and sets its value attribute to the name of the target. This transformation occurs separately for all three targets, resulting in 3 app.config files, each with a different browser key (that is available to your application).

WebDriver.cs (located in the Drivers folder of the TestApplication.UiTests project) uses this key to instantiate a web driver of the appropriate type (e.g. InternetExplorerDriver). The key from the configuration file is passed to BrowserConfig, used in the switch statement:

get
{
  if (_currentWebDriver != null)
    return _currentWebDriver;

  switch (BrowserConfig)
  {
    case "IE":
      _currentWebDriver = new InternetExplorerDriver(new InternetExplorerOptions() { IgnoreZoomLevel = true }) { Url = SeleniumBaseUrl };
      break;
    case "Chrome":
      _currentWebDriver = new ChromeDriver() { Url = SeleniumBaseUrl };
      break;
    case "Firefox":
      _currentWebDriver = new FirefoxDriver() { Url = SeleniumBaseUrl };
      break;
    default:
      throw new NotSupportedException($"{BrowserConfig} is not a supported browser");
    }

  return _currentWebDriver;
}

Depending on the target, the driver is instantiated as either the InternetExplorerDriver, ChromeDriver or FirefoxDriver driver type. The bindings code simply uses the required web driver for the target to execute the test; there is no need to write separate tests for each browser. You can see this at work in the Browser.cs and CalculatorFeatureSteps.cs files:

[Binding]
public class CalculatorFeatureSteps
{
  private readonly WebDriver _webDriver;

  public CalculatorFeatureSteps(WebDriver webDriver)
  {
    _webDriver = webDriver;
  }
       
  [Given(@"I have entered (.*) into (.*) calculator")]
  public void GivenIHaveEnteredIntoTheCalculator(int p0, string id)
  {
    _webDriver.Wait.Until(d => d.FindElement(By.Id(id))).SendKeys(p0.ToString());
  }

To ensure that the tests are executed, you still need to ensure that the tests have the appropriate tags (@Browser_Chrome, @Browser_IE, @Browser_Firefox). 2 scenarios have been defined in CalculatorFeature.feature:

@Browser_Chrome
@Browser_IE
@Browser_Firefox
Scenario: Basepage is Calculator
	Given I navigated to /
	Then browser title is Calculator

@Browser_IE 
@Browser_Chrome
Scenario Outline: Add Two Numbers
	Given I navigated to /
	And I have entered <SummandOne> into summandOne calculator
	And I have entered <SummandTwo> into summandTwo calculator
	When I press add
	Then the result should be <Result> on the screen

Scenarios: 
		| SummandOne | SummandTwo | Result |       
		| 50         | 70         | 120    | 
		| 1          | 10         | 11     |

Using targets in this way can significantly reduce the number of tests you need to write and maintain. You can reuse the same test and bindings for multiple browsers. Once you’ve set up your targets and web driver, all you need to do is tag your scenarios correctly. If you select “Traits” under Group By Project in the Test Explorer, the tests are split up by browser tag. You can easily run a test in a particular browser and identify which browser the tests failed in. The test report generated by SpecFlow+ Runner also splits up the test results by target/browser.

Remember that targets can be used for a lot more than executing the same test in multiple browsers with Selenium. Don’t forget to read the documentation on targets, as well as the sections on filters, target environments and deployment transformations.