There's plenty of guides online describing what Selenium does and how to get up and running with Visual Studio and SpecFlow - so I won't go into too much detail here. I want to focus on maintainability and reusability. I've stayed away from using any of the Selenium wrappers for this post, and will be blogging about those in the future.
Start by doing the following:
- Create a new Class Library Project
- Add the SpecFlow extension to Visual Studio
Install the following via NuGet:
-Selenium.WebDriver
-Selenium.Support
-WebDriver.ChromeDriver
-SpecFlow.NUnit
-SpecRun.SpecFlow
Your App.Config should look something like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
</configSections>
<specFlow>
<unitTestProvider name="SpecRun+NUnit" />
<plugins>
<add name="SpecRun" />
</plugins>
</specFlow>
</configuration>
Add the ChromeDriver.exe location to the AppSettings section of the app.config - other browser drivers would be added in the same way, so we only edit one place if we need to change the location:
<appSettings>
<add key="chromeDriver" value="C:\Users\Administrator\Documents"/>
</appSettings>
SpecFlow Attributes
Adding the [Binding] attribute to a class lets SpecFlow know that the class contains Step Definitions. This attribute needs to be on every class that contains code required for the tests to execute correctly.
SpecFlow has a number of attributes or "hooks" that can be used to decorate methods, these allow for methods to be called at certain times. For instances methods decorated with the [BeforeScenario] and [AfterScenario] attributes would be called before and after the scenario has executed.
Multiple attributes of the same name are executed in an unpredictable order, to get around this you can set an order similar to [BeforeScenario(Order=1)].
Attributes can also support tag filtering, meaning only scenarios that are prefixed with an @mytagname in the Feature file will run the decorated method [AfterScenario("mytagname")].
More detail on SpecFlow attributes can be found HERE.
Test Setup
Create a test setup base class - I call mine TestSetup. This class is used in conjunction with a number of SpecFlow's attributes to initialise the browser before the test begins and perform any tasks at the end of the scenario, such as disposing the browser, calling code to take a screenshot if the test failed, and setting the database back to its original state at the end of the test.
public class TestSetup
{
public static IWebDriver Browser { get; private set; }
[BeforeScenario]
private void InitialiseScenario()
{
try
{
var options = new ChromeOptions {BinaryLocation = @"C:\Google\Application\chrome.exe"};
Browser = new ChromeDriver(ConfigurationManager.AppSettings["chromeDriver"], options);
}
catch (Exception ex)
{
throw new Exception("Unable to locate chrome driver - check the App.config to ensure the directory is correct for chromeDriver!");
}
}
[AfterScenario]
private void TearDownScenario()
{
Browser.Dispose();
}
[AfterScenario]
private void ShowErrors()
{
if (ScenarioContext.Current.TestError != null)
{
//ToDo Add some code to capture screenshot.
}
}
[BeforeScenario("resetDatabase")]
[AfterScenario("resetDatabase")]
private void ResetDatabase()
{
//ToDo run SQL script to put database in original state.
}
}
Note: The [Binding] attribute is not required on the TestSetup class as we are going to inherit it. The derived class will have the [Binding] attribute, if both have it you may find 2 browser instances are created each time you run the tests
The IWebDriver property is the WebDriver interface which controls the browser and allows you to interact with elements on the page. You will also notice that the InitialiseScenario() method assigns this interface to a new ChromeDriver instance, which utilises the App.Config setting we previously set up. Any change to driver type would be made here (as additional app settings in the config).
We will come back to this base class once we create the Page Objects.
Page Objects
Using Page Objects allows you to separate the implementation from the specification - this makes your tests less brittle and easier to read. A Page Object represents a web page (or part of a web page) and by utilising a Page Object you can place all the logic required to interact with part of a web page into a single location, improving maintainability.
The Page Object encapsulates all the logic surrounding page interactions, allowing your tests to concentrate on what to do rather than how to do it.
For this example we will use Wikipedia.com. I've added the default url into the App.config as follows(under the chromeDriver setting I added earlier):
<add key="MainPageUrl" value="https://www.wikipedia.org/"/>
Begin by creating an abstract class, I call it PageObject. This class will contain common web page properties such as its Title and URL. This class can also contain methods to fill out all fields with values detailed in the test, or with default values for each field on the page - useful to progress through a multi page website, and also move most form filling actions into a single method containing a switch statement to decide how each field is selected (I'll blog about this in the future).
For now I've created the following:
public abstract class PageObject
{
internal string Title
{
get { return Browser.Title; }
}
internal static string Url { get; set; }
protected static IWebDriver Browser { get; set; }
protected PageObject(IWebDriver driver)
{
Url = ConfigurationManager.AppSettings[(GetType().Name) + "Url"];
Browser = driver;
GotoPageUrl();
}
public void GotoPageUrl()
{
try
{
Browser.Navigate().GoToUrl(Url);
}
catch (Exception e)
{
throw new Exception(GetType().Name + " could not be loaded. Check page url is correct in app.config." +
" " + e.Message);
}
}
}
NOTE: you may need to add a reference to System.configuration.dll if it's not added automatically
This class retrieves the relevant page URL from the App.config using (GetType().Name) + "Url" so if we have an inheriting class called MainPage, GetType().Name will retrieve the class name as a String and the code then concatenates this class name and "Url" to become MainPageUrl. This then matches the app setting we just added to the App.Config.
Next I create a Page Object to represent the Wikipedia main page, called MainPage, which inherits from PageObject:
public class MainPage : PageObject
{
public MainPage(IWebDriver driver) : base(driver)
{
}
}
Notice that the MainPage constructor passes the WebDriver instance to its base PageObject which will in turn navigate the browser to the pages Url.
IWebElement & FindsBy
Now I've created a Page Object to represent the Wikipedia main page I need to add the elements (fields, buttons, etc.) as properties. This is achieved by firstly adding an appropriately named IWebElement property to the MainPage class:
private IWebElement _searchField;
I then decorate the property with the FindsBy attribute. This attribute is part of the OpenQA.Selenium.Support.PageObjects namespace and allows an element to be located on the web page by a number of means including Id, CssSelector, ClassName or Xpath and assigned to the property. You can also create a custom method if required.
[FindsBy(How = How.Id, Using = "searchInput")]
private IWebElement _searchField;
For this example I will use only 2 elements on the page, so I've added a second property for the search button:
[FindsBy(How = How.Name, Using = "go")]
private IWebElement _searchButton;
In reality your web page will have many elements, your Page Objects might then represent sections of the page rather than whole pages in order to improve maintainability.
Now we need 2 methods, one to enter text into the Search field and one to click the Search button:
public void EnterSearchText(string searchText)
{
_searchField.SendKeys(searchText);
}
public void ClickSearchButton()
{
_searchButton.Click();
}
Again for this example I'll keep it simple and add these methods to the MainPage object, but once you start creating additional Page Objects you will want to pull this common functionality out, possibly into the base PageObject class, for reusability and maintainability. This will be covered in the next blog post.
SpecFlow Feature File & Scenario
The next step is to add a Feature file to the project and add a new Scenario:
Feature: MainPage
In order to further my test automation knowledge
As an automation tester
I want to search for information on Wikipedia
Scenario: Search for Test Automation on Wikipedia
Given I am on the main Wikipedia page
And I enter Test Automation into the Search field
When I click the Search button
Then the Test automation wikipedia page is displayed
Right click the scenario text and select Generate Step Definitions, you can now either copy the methods to clipboard and paste them into your own class (make sure you add the [Binding] attribute to the class) or click the Generate button . This will create a new class (mine is called MainPageSteps.cs) that contains the following:
[Given(@"I am on the main Wikipedia page")]
public void GivenIAmOnTheMainWikipediaPage()
{
ScenarioContext.Current.Pending();
}
[Given(@"I enter Test Automation into the Search field")]
public void GivenIEnterTestAutomationIntoTheSearchField()
{
ScenarioContext.Current.Pending();
}
[When(@"I click the Search button")]
public void WhenIClickTheSearchButton()
{
ScenarioContext.Current.Pending();
}
[Then(@"the Test Automation wikipedia page is displayed")]
public void ThenTheTestAutomationWikipediaPageIsDisplayed()
{
ScenarioContext.Current.Pending();
}
I can now make some changes to the methods which should allow reuse in the future:
[Given(@"I am on the main Wikipedia page")]
public void GivenIAmOnTheMainWikipediaPage()
{
ScenarioContext.Current.Pending();
}
[Given(@"I enter (.*) into the Search field")]
public void GivenIEnterTestAutomationIntoTheSearchField(string input)
{
ScenarioContext.Current.Pending();
}
[When(@"I click the (.*) button")]
public void WhenIClickTheSearchButton(string butt)
{
ScenarioContext.Current.Pending();
}
[Then(@"the (.*) wikipedia page is displayed")]
public void ThenTheTestAutomationWikipediaPageIsDisplayed(string page)
{
ScenarioContext.Current.Pending();
}
The wildcards added mean as long as the Given, When or Then statement wording matches then the (.*) will be taken as an input parameter into the method.
Now to finish off the Scenario all the ScenarioContext.Current.Pending() lines are replaced with actual logic to perform the test and your class should look like this:
[Binding]
public class MainPageSteps : TestSetup
{
private MainPage PageToTest { get; set; }
[Given(@"I am on the main Wikipedia page")]
public void GivenIAmOnTheMainWikipediaPage()
{
PageToTest = new MainPage(Browser);
}
[Given(@"I enter (.*) into the Search field")]
public void GivenIEnterTestAutomationIntoTheSearchField(string input)
{
PageToTest.EnterSearchText(input);
}
[When(@"I click the (.*) button")]
public void WhenIClickTheSearchButton(string butt)
{
PageToTest.ClickSearchButton();
}
[Then(@"the (.*) wikipedia page is displayed")]
public void ThenTheTestAutomationWikipediaPageIsDisplayed(string page)
{
Assert.AreEqual(page + " - Wikipedia, the free encyclopedia", PageToTest.Title);
}
That's it for now, these tests should run by right clicking within the Feature file and selecting "Run SpecFlow Scenarios". In my next blog post i'll reuse this code, refactor further and implement a Selenium wrapper called Coypu.
I'll be uploading the source code over the weekend for anyone who's interested and will provide a link here.
Software Development & Testing Blog