Monday, October 20, 2014

How we're writing CodedUI tests

A couple of sprints ago I had a bit too much time on my hands and our tester was short on time. Therefore, my colleague and me decided to help him out by writing some of the CodedUI tests for him.

We started out by looking at the way they had been doing it thusfar, but we could see some room for improvement. So we set out two write CodedUI tests that were:

  1. Easy to maintain
  2. Supported re-use of steps
  3. Easy to read

This is what we ended up with:

[TestMethod]
public void SomeScreen_Should_ShowStufIfSomething()
{
    var category = "Whatever";

    var page = new LoginPage(this.TestedPage.BrowserInstance)
        .LoginAsAdmin()
        .GoToOtherScreen()
        .SearchForAccountNumber(ValidAccountNumber)
        .ClickOnFirstResult()
        .ClickOnModelDistributionEditLink(category)
        .SetModelDistribution(null);

    page.ModelRows.Single(x => x.Category == category).HasSomething.Should().BeFalse();
}
Inspiration to write code like this came from this blogpost and this library.

What we do is we create a class per page. We create a couple of partials for that class, one for the page.cs, page.operations.cs and page.elements.cs.

The page.cs only contains the page URL, and in some cases some more stuff like items that indicate that the page is done loading as soon as they appear:

public partial class LoginPage : TestPageBase
{
    public LoginPage(){}
    public LoginPage(BrowserWindow browser): base(browser){}

    public override string PageUri
    {
        get { return "/"; }
    }
}
Then we have the .elements.cs file which contains read-only properties that point to elements on the page:
public partial class LoginPage
{
    private HtmlComboBox BankOrRoleDropDown
    {
        get { return Browser.FindAllById("bank", PropertyExpressionOperator.Contains).FirstOrDefault(); }
    }
    private HtmlEdit PasswordBox
    {
        get { return Browser.FindAllByName("ww").FirstOrDefault(); }
    }
}
And we've got the operations.cs file:
public partial class LoginPage : TestPageBase
{

    public HomePage Login(string role, string password)
    {
        this.ClearCookies();

        this.NavigateTo(BaseUrl);
        Thread.Sleep(500);
        this.BankOrRoleDropDown.SelectedItem = role;
        this.PasswordBox.Text = password;
        this.PasswordBox.SetFocus();
        Keyboard.SendKeys("{Enter}");

        return new HomePage(this.Browser);
    }

    public HomePage LoginAsAdmin()
    {
        return this.Login("Username", "Password");
    }
}
The tests just use sequences of the page.operations methods which are linted together because we implemented it fluently. In order to make querying the page a bit more easy, we wrote a generic helper, which you can find below. Lesson for use devs was to put more classes and id's on stuff that needed to be identified. Now, since all the testers adopted this way of writing CodedUI tests, the developers contribute in making the .elements.cs files for a page they made, so that writing the codedUI operations become a breeze.
public static class BrowserWindowExtensions
{

    public static T FindFirstByClass(this UITestControl browser, string @class, PropertyExpressionOperator searchOption = PropertyExpressionOperator.Contains) where T : UITestControl, new()
    {
        return browser.FindAllByClass(@class, searchOption).FirstOrDefault();
    }

    public static T FindFirstById(this UITestControl browser, string id, PropertyExpressionOperator searchOption = PropertyExpressionOperator.EqualTo) where T : UITestControl, new()
    {
        return browser.FindAllById(id, searchOption).FirstOrDefault();
    }

    public static IEnumerable FindAllById(this UITestControl browser, string id, PropertyExpressionOperator searchOption = PropertyExpressionOperator.EqualTo) where T : UITestControl, new()
    {
        return browser.FindAllByKeyCharacteristic("id", id, searchOption);
    }
    public static IEnumerable FindAllByClass(this UITestControl browser, string @class, PropertyExpressionOperator searchOption = PropertyExpressionOperator.Contains) where T : UITestControl, new()
    {
        return browser.FindAllByKeyCharacteristic("class", @class, searchOption);
    }
    public static IEnumerable FindAllByName(this UITestControl browser, string name, PropertyExpressionOperator searchOption = PropertyExpressionOperator.EqualTo) where T : UITestControl, new()
    {
        return browser.FindAllByKeyCharacteristic("name", name, searchOption);
    }

    public static IEnumerable FindAllByKeyCharacteristic(this UITestControl browser, string keyCharacteristic, string keyCharacteristicValue, PropertyExpressionOperator searchOption) where T : UITestControl, new()
    {
        var element = (T)Activator.CreateInstance(typeof(T), browser);
        element.SearchProperties.Add(keyCharacteristic, keyCharacteristicValue, searchOption);
        var result = element.FindMatchingControls().ToTypedList().ToList();
        return result;
    }

    public static IEnumerable ToTypedList(this UITestControlCollection collection) where T : UITestControl, new()
    {
        var result = new List();
        collection.ToList().ForEach(control => result.Add((T)control));
        return result;
    } 
}

No comments:

Post a Comment