Click here to monitor SSC

Test Engineer at Red Gate Software Ltd.

Improving the xUnit.net [Browser] attribute

Published 2 September 2009 4:38 am

In yesterday’s post I talked about an xUnit [Browser] attribute that wraps Selenium, taking care of setting up the browser objects we pass as parameters into our test methods. Where I left off, my example test case looked as follows:

[Theory]
[Browser("Internet Explorer 7")]
[Browser("Firefox 3.5")]
[Browser("Google Chrome")]
[Browser("Opera")]
public void Google_For_SimpleTalk(ISelenium iSelenium)
{
    Browser = iSelenium;

    Browser.Open("/");
    Browser.Type("q", "Simple Talk");
    Browser.Click("btnG");

    Browser.WaitForPageToLoad("5000");

    Assert.True(Browser.IsTextPresent("www.simple-talk.com"));
}

This worked because the BrowserAttribute class had ‘http://www.google.co.uk‘ hard coded as the URL passed into the DefaultSelenium constructor. Clearly hard-coding the starting point for each test is bad, since you would then need to navigate to the page you are interested in testing at the beginning of every test method.

Instead, it would be far more flexible to be able to pass the start page for any individual test into the DefaultSelenium constructor, via the [Browser] attribute. To do this, we simply change the BrowserAttribute code to the below:

public class BrowserAttribute : DataAttribute
{
private ISelenium Browser { get; set; }

public BrowserAttribute(string browser, string url)
{
switch (browser)
{
    case "Internet Explorer 7":
        Browser = new DefaultSelenium("localhost", 4444, "*iexplore", url);
        Browser.Start();
        break;
    case "Firefox 3.5":
        Browser = new DefaultSelenium("localhost", 4444, "*firefox", url);
        Browser.Start();
        break;
    case "Google Chrome":
        Browser = new DefaultSelenium("localhost", 4444, "*googlechrome", url);
        Browser.Start();
        break;
    case "Opera":
        Browser = new DefaultSelenium("localhost", 4444, "*opera", url);
        Browser.Start();
        break;
}
}

public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
{
    return new[] { Browser };
}
}

So now the attribute accepts two strings, one each for the browser and start URL. This means our test now becomes:

[Theory]
[Browser("Internet Explorer 7", "http://www.google.co.uk")]
[Browser("Firefox 3.5", "http://www.google.co.uk")]
[Browser("Google Chrome", "http://www.google.co.uk")]
[Browser("Opera", "http://www.google.co.uk")]
public void Google_For_SimpleTalk(ISelenium iSelenium)
{
    Browser = iSelenium;

    Browser.Open("/");
    Browser.Type("q", "Simple Talk");
    Browser.Click("btnG");

    Browser.WaitForPageToLoad("5000");

    Assert.True(Browser.IsTextPresent("www.simple-talk.com"));
}

At first glance this looks like we’re unnecessarily duplicating test data. But it feels like a hit worth taking, since we can now decorate our test methods as follows:

[Theory]
[Browser("Google Chrome","http://SiteUnderTest/Account/Login.aspx")]
public void Login_Works_With_Valid_Credentials() {}

[Theory]
[Browser("Firefox 3.5","http://SiteUnderTest/Account/Register.aspx")]
public void Signup_Rejects_Invalid_Email_Addresses(){}


The second issue I highlighted last time was that of browser versions.

One of the nice things that Selenium gives you is the ability to offload the execution of your test cases to another machine or machines. This is done using Selenium RC (“Remote Control”).

Briefly, Selenium RC works by running a server process on each machine that you want to execute tests on. This will handle launching browser instances, executing your test cases, and acts as a proxy between the browser and the site being tested, allowing it to intercept and verify the traffic passing between them.

To try this out, I fired up three virtual machines on one of testing servers. Between them, these machines cover:

  • Internet Explorer 6
  • Internet Explorer 7
  • Internet Explorer 8
  • Firefox 2
  • Firefox 3
  • Firefox 3.5
  • Opera 9
  • Google Chrome 2

This combination covers about ~96% of our total visitors to Simple Talk.

The Selenium RC server is written in Java (for cross-platform purposes), so it was necessary to first ensure that each VM had the Java Runtime installed. I also had to ensure that they were joined to our test network domain, otherwise I would have to resort to referencing them by their (ever-changing) IP address. The final (but not strictly necessary) preparation step was to write a short batch file to start the server process at boot time.

To start using the VMs instead of my local machine, I had to change how I was calling the DefaultSelenium constructor. Where before I was passing “localhost” for the serverUrl parameter, we now specify the machine name of the VM running that particular version of the browser in question. I won’t list the entire set of cases in the switch block, as its getting quite long now, but below shows the change for the three versions of Internet Explorer.

public BrowserAttribute(string browser, string url)
        {
            switch (browser)
            {
                case "Internet Explorer 6":
                    Browser = new DefaultSelenium("testrunner1", 4444, "*iexplore", url);
                    Browser.Start();
                    break;
                case "Internet Explorer 7":
                    Browser = new DefaultSelenium("testrunner2", 4444, "*iexplore", url);
                    Browser.Start();
                    break;
                case "Internet Explorer 8":
                    Browser = new DefaultSelenium("testrunner3", 4444, "*iexplore", url);
                    Browser.Start();
                    break;

At this point I also decided to pull the magic browser strings out into a static ‘Browsers’ class, so that we do not need to know their values, and can instead select browsers via Intellisense:

image


With those changes made, I looked to address the final issue highlighted last time out – the way the browser objects were being created. By creating the Browser object and calling it’s Start() method within the BrowserAttribute’s constructor, I was causing xUnit to start up all the requested browser instances before any test run was started. To fix this, I simply need to Start() the browser from within my override of GetData() instead (thanks must go to Mark, for pointing me in this direction). In order to do that, I moved my switch block out of the constructor into a new CreateBrowser() method, which returns the ISelenium object. The constructor now simply accepts and stores the browser and URL strings locally, where they are subsequently picked up by CreateBrowser(). The GetData() method now yield returns a call to CreateBrowser():

public class BrowserAttribute : DataAttribute
{
private ISelenium Browser { get; set; }
private string _browser { get; set; }
private string _url { get; set; }

public BrowserAttribute(string browser, string url)
{
    _browser = browser;
    _url = url;
}

private ISelenium CreateBrowser()
{
switch (_browser)
{
    case "Internet Explorer 6":
        Browser = new DefaultSelenium("testrunner1", 4444, "*iexplore", _url);
        Browser.Start();
        break;
    case "Internet Explorer 7":
        Browser = new DefaultSelenium("testrunner2", 4444, "*iexplore", _url);
        Browser.Start();
        break;
    case "Internet Explorer 8":
        Browser = new DefaultSelenium("testrunner3", 4444, "*iexplore", _url);
        Browser.Start();
        break;
    case "Firefox 2":
        Browser = new DefaultSelenium("testrunner1", 4444, "*firefox", _url);
        Browser.Start();
        break;
    case "Firefox 3":
        Browser = new DefaultSelenium("testrunner2", 4444, "*firefox", _url);
        Browser.Start();
        break;
    case "Firefox 3.5":
        Browser = new DefaultSelenium("testrunner3", 4444, "*firefox", _url);
        Browser.Start();
        break;
    case "Google Chrome":
        Browser = new DefaultSelenium("testrunner1", 4444, "*googlechrome", _url);
        Browser.Start();
        break;
    case "Opera":
        Browser = new DefaultSelenium("testrunner3", 4444, "*opera", _url);
        Browser.Start();
        break;
}
return Browser;
}

public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
{
    yield return new[] { CreateBrowser() };
}
}

We now have everything we need to write concise integration tests, exercising the vast majority of browsers that our site sees visits from. I should also point out that, whilst the VMs I use are all running XP, Selenium RC works on any platform that can run the Java Runtime Environment. What’s more, there is no difference whatsoever from the point of view of your test code. So if you get a lot of traffic from MacOS users browsing in Safari, or Linux-based users with Konquerer, its as simple as starting an instance of the RC server on a MacOS or Linux machine, and using its name when you create your DefaultSelenium object. Selenium will handle the rest!

This is a relatively new pursuit for us, so if anybody has any tips, or can point out potential pit falls, I’m all ears!

2 Responses to “Improving the xUnit.net [Browser] attribute”

  1. Anonymous says:

    There has been a fair amount of discussion about how to improve our test automation here in the Web Team…

  2. Anonymous says:

    After my last post it was suggested to me that I create a [URL] attribute to deal with the unnecessary…

Leave a Reply