I have recently been running a trial of Selenium to automate some of our regression and integration testing. I have only been looking into this for a short amount of time so I am by no means an expert but this post contains a few of my observations so far.
For those of you that are not familiar with it, Selenium is a browser automation system that allows you to write integration tests to control a browser and check the response of your site. An example of a Selenium script might look like this:
- Open the browser
- Browse to the login page
- Enter “user 1” in the input with ID #username
- Enter “pa$$word” in input with ID #password
- Click the Login button and wait for the page to load
- Check that the browser has navigated to the users home page
Selenium as a framework comes in 2 flavours: IDE & WebDriver.
Selenium IDE
IDE uses a record-and-playback system to define the script and to run the tests. It is implemented as a FireFox plugin and is therefore limited to FireFox only.
We had run a previous trial using this version where we attempted to have our QA team record and execute scripts as part of functional and regression testing. We found that this had a number of problems and eventually abandoned the trial:
- Limited to FireFox
- Has to be run manually (i.e. Cannot be run automatically on a build server)
- Often requires some basic understanding of JavaScript or CSS selectors to work through a problem in a script; this was sometimes beyond the technical knowledge of our QA team
- Automatically-generated selectors are often extremely fragile. Instead of input#password, it might generate body > div.main-content > form > input:last-child. This meant that a lot of time was lost to maintenance and that the vast majority of “errors” reported by the script were actually incorrect selectors.
We decided that there we too many disadvantages with this option and so moved onto Selenium WebDriver.
Selenium WebDriver
WebDriver requires that all scripts are written in the programming language of your choice. This forced the script-writing task onto our development team instead of QA, but also meant that development best-practices could be employed to improve the quality and maintainability of the scripts.
This version of Selenium also (crucially) supports multiple browsers and can be run as part of an automated nightly build so seemed like a much better fit.
Whilst writing our first few Selenium tests we came up with a few thoughts on the structure
Use a Base Fixture for Multiple Browser Testing
This is a nice simple one - we did not want to write duplicate tests for all browsers so we made use of the Generic Test Fixture nUnit feature to automatically run our tests in the 4 browsers in which we were interested.
We created a generic base fixture class for all our tests and decorated it with the attribute. This instructs nUnit to instantiate and run the class for each of the specified generic types, which in turn means any test we write in such a fixture will automatically be run against each browser
[TestFixture(typeof(ChromeDriver))]
[TestFixture(typeof(InternetExplorerDriver))]
[TestFixture(typeof(FirefoxDriver))]
public abstract class SeleniumTestFixtureBase<TWebDriver>
where TWebDriver : IWebDriver
{
protected IWebDriver Driver { get; private set; }
[SetUp]
public void CreateDriver()
{
this.Driver = DriverFactory.Instance
.CreateWebDriver<TWebDriver>();
//...
}
}
This does have some disadvantages when it comes to debugging tests as there are always 4 tests with the same method name but this has only been a minor inconvenience so far - the browser can be determined from the fixture class name where needed.
Wrap Selectors in a “Page” Object
The biggest problem with our initial trial of “record and playback” automated tests was the fragility of our selectors. Tests would regularly fail when manual testing would demonstrate the feature clearly working, and this was almost always due to a subtle change in the DOM structure.
If your first reaction to a failing test is to say “the test is probably broken” then your tests are useless!
A part of the cause was that the “record” part of the feature does not always select the most sensible selector to identify the element. We assumed that by hand-picking selectors we would automatically improve the robustness (is that a word?) of our selectors, but in the case where they did change we still did not want to have to update a lot of places. Similarly, we did not want to have to work out what a selector was trying to identify when debugging tests.
Our solution to this was to create a “Page” object to wrap the selectors for each page on the site in meaningfully named methods. For example, our LoginPage class might look like this:
public class LoginPage
{
private IWebDriver _driver;
public LoginPage(IWebDriver driver)
{
_driver = driver;
}
public IWebElement UsernameInput()
{
return _driver.FindElement(By.CssSelector("#userName"));
}
public IWebElement PasswordInput()
{
return _driver.FindElement(By.CssSelector("#Password"));
}
}
This has a number of advantages: _ Single definition of the selector for a given DOM element
We only ever define each element once _ Page inheritance
We can create base pages that expose page elements which appear on multiple pages (e.g. the main navigation or the user settings menu) * Creating helper methods
When we repeat blocks of functionality (e.g. Enter [usename]
, enter [password]
then click Submit) we are able to encapsulate them on the Page class instead of private methods within the test fixture.
We also created factory extension methods on the IWebDriver element to improve readability
public static class LoginPageFactory
{
public static LoginPage LoginPage(this IWebDriver driver)
{
return new LoginPage(driver);
}
}
//...
this.Driver.LoginPage().UsernameInput().Click()
Storing Environment Information
We decided to store our environmental variables in code to improve reuse and readability. This is only a minor point but we did not want to have any URLs, usernames or configuration options hard coded in the tests.
We structured our data so we could reference variables as below:
TestEnvironment.Users.AdminUsers[0].Username
Switching between Debug & Release Mode
By storing environment variables in code we created another problem: how to switch between running against the test environment and against the local developer environment.
We solved this by loading certain changeable elements of our configuration from .config files based on a #DEBUG
flag
Other Gotchas
- The 64bit IE driver for Selenium IDE is incredibly slow! Uninstall it and install the 32-bit one
- Browser locale can - in most cases - be set using a flag when creating the driver. One exception to this is Safari for Windows, which does not seem to allow you to change the locale at all - even through Safari itself!
Summary
We are still in the early phases of this trial but it is looking like we will be able to make Selenium automation a significant part of our testing strategy going forward.
Hopefully these will help out other people. If you have any suggestions of your own then leave them in the comments on message me on Twitter (@stevegreatrex).