Architecting UI Automation Projects for Maintainability

April 17, 2018    Development UnitTesting DevOps AutomatedTesting Selenium

Architecting UI Automation Projects for Maintainability

Originally posted on November 10, 2015, but it still applies to how I view things in 2018.

I’ve been on a team now for a year+ and we’ve been using Selenium to automate acceptance tests before we consider a feature as completed. We’ve caught many regression issues that our Jasmine unit tests haven’t (which sometimes is a gap in our Jasmine tests, and sometimes is just a Knockout binding issue) and have avoided QA picking up buggy software for manually testing.

I was reading Specifications by Example Chapter 9 “Automating validation without changing specifications”. The book deserves its own article, several actually, is worth the time to read (and discuss with others) and we aren’t practicing it on my current team, but I’d like to implement/introduce ideas from it and also apply things as I can. He interviewed many teams and combines a lot of lessons learned, so that is very valuable.  We’ve learned some of the same lessons that are pointed out in the book about treating automation code like you would treat the application code (re-factor, follow good patterns, etc).

See my code that I started for a presentation I did at South Dakota Code Camp in Sioux Falls, SD on November 7th, 2015. This example uses the ASP.Net file new project MVC, automates the register action and verifies that it works

Test Automation Layer

Methods in this layer are in the [TestMethod] methods (in MSTest) and don’t have any direct interactions to the DOM.

Example: in the RegisterTest.cs

[TestMethod]
[TestCategory(TestCategories.Registration)]
public void UserCanRegisterTest()
{
    this.CurrentBrowserManager =  new BrowserManager();
    this.CurrentBrowserManager.Launch(BaseUri);
    var homePage = new HomePage();

    // we end up back on the home page
    string userName = "ben_" \+ DateTime.Now.Ticks + "@jump.com";
    homePage = homePage.RegisterUser(userName, "Pa$$word1");
    var helloMessage = homePage.GetAuthenticatedHeaderMessage();
    Assert.IsTrue(helloMessage.Contains(userName));
}

Technical / mappings

Do all of the mappings to the DOM inside of classes.

Notice the private methods the return an IWebElement such as the GetLoginLink(). Keeping the FindElement calls in methods makes it much easier to update, find trouble spots, and maintain. We’ve also found that you should not let the IWebElement leak outside of the class. Return object wrappers instead.

Example: HomePage.cs

    /// <summary>
    /// UI Mapping for the Home Page. This is a wrapper for all UI interactions.
    /// </summary>
    public class HomePage : BaseMappingPage
    {
        public HomePage Login(string username, string password)
        {
            this.GetLoginLink().Click();
            var loginPage = new LoginPage();
            loginPage.Login(username, password);
            return new HomePage();
        }
        private IWebElement GetLoginLink()
        {
            return this.Driver.FindElement(By.Id("loginLink"));
        }

        public HomePage RegisterUser(string userName, string password)
        {
            this.GetRegisterLink().Click();
            var registerPage = new RegisterPage();
            return registerPage.RegisterUser(userName, password);
        }

        private IWebElement GetRegisterLink()
        {
            return this.Driver.FindElement(By.Id("registerLink"));
        }

        public string GetAuthenticatedHeaderMessage()
        {
            var element = this.Driver.FindElement(By.Id("auto-AuthenticatedHeaderHello"));
            return element == null ? string.Empty : element.Text;
        }
    }

Workflow Methods

It is very convenient to be able to call homePage.RegisterUser in the test method, instead of having to call all the steps in the TestMethod. It keeps things cleaner, you can re-use the steps, and changes can be made easier. Treat your test code like production code and follow good programming practices.

Example: RegisterPage.RegisterUser

/// <summary>
/// Register the user with the given username and password.
/// Redirected to homepage after success.
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns></returns>
public HomePage RegisterUser(string userName, string password)
{
    this.GetEmailInput().SendKeys(userName);
    this.GetPasswordOneInput().SendKeys(password);
    this.GetConfirmPasswordInput().SendKeys(password);
    this.GetSubmitButton().Click();

    // NOTE: building in a loading indicator and waiting for the div to 
    // be removed from the page will help you avoid timing issues in your tests
    // for example: the UI may run faster than your web server and browser processes
    // and it will try to click on a link that isn't loaded yet.
    return new HomePage();
}

Driver, Browser, Tools Project

Use base classes and other classes to handle the WebDriver and the browser interactions.

Example: I’m extending from BaseMappingPage, this can grow as needs arise.

public class BaseMappingPage
{
    protected RemoteWebDriver Driver => BrowserManager.Driver;
}

Example 2: BrowserManager has a static property of the Driver. I used static to avoid having to pass it in to every class. There may be a better way here.

/// <summary>
/// Manage the browser instances for Selenium tests.
/// </summary>
public class BrowserManager
{
    public static RemoteWebDriver Driver { get; private set; }

    /// <summary>
    /// Launch must be called in order to populate the browser and open it.
    /// </summary>
    /// <param name="baseUri"></param>
    /// <param name="browserType"></param>
    public void Launch(string baseUri, BrowserType browserType = BrowserType.Firefox)
    {
        Driver = BrowserDriverFactory.CreateDriver(browserType);
        Driver.Navigate().GoToUrl(baseUri);
    }

    public void Quit()
    {
        Driver.Quit();
    }
}

Avoid Thread.Sleep

We’re using KnockoutJs to create dynamic DOM and sometimes the test fails because the element is there yet or it tries to click before it is ready. The first approach was to add Thread.Sleep(2000), but that has a load of problems. What if it takes 2500ms? What if it takes 500ms? Then you’re slowing down your already long running test run. It’s better to write a for loop that checks for a loading indicator to disappear. It turns out that showing loading or working indicators is good for users as well when lag or slower connections are reality.

“Automate Below the Skin”

UI tests take longer to run and more work to maintain. Sometimes you can hit the API directly and avoid having to click on the UI button.

You should have more unit tests than UI/acceptance tests.

Remember the testing pyramid:

pyramid

(image from http://www.ontestautomation.com/tag/test-automation-pyramid/)

Hopefully these hints will get you going and avoid some of the pitfalls we ran into in the last few years.



comments powered by Disqus

Please consider using Brave and adding me to your payment ledger. Then you won't have to see ads!

Support me and download Brave!

Use Brave