Selenium Would Be Great If I Didn't Have To Design Around It, Part 2

So, last time, I dug into how Selenium blows up if you ask it for something that doesn't exist. I mean, how dare I, the tester, want to care about things that don't exist? If we asked about all the things that don't exist, we'd never have time to play ping pong. Never the less, we are asked to test these nasty requirements about security and privacy and some times you really don't want to show private data to every user that hits your page. So, here we are.

Today's Selenium sin is the Stale Element Exception. This is a thing that should never exist. To even understand why it exists, you need to stew yourself in a bath of object references and multi-process timing. That, my friends, is a bath you do not want to sit in. Let me do my best to give an example of why we might see this problem.

Let's say you have a web application that displays a dialog to edit a user. You want to make sure a certain user property is displayed on a couple users on the edit users page. In the code example below, you stashed away all the element getters in a lovely page object/application model that you've lovingly crafted because you're a considerate person. You're procedure looks something like this:

// Step 1: Click edit on Boaty McBoatface
Application.EditUserPage.EditButton("Boaty McBoatface").Click();

// Step 2: Make sure Boaty likes penguins
Assert.AreEqual("penguins", Application.EditUserPage.EditDialog.LikeBox.Text);

// Step 3: Close the dialog
Application.EditUserPage.EditDialog.CloseButton.Click();

// Step 4: Click edit on Hooty McOwlface
Application.EditUserPage.EditButton("Hooty McOwlface").Click();

// Step 5: Make sure Hooty likes whales
Assert.AreEqual("whales", Application.EditUserPage.EditDialog.LikeBox.Text);

You run this procedure and it works great. You run this procedure 10 times and it hums along like a champ every time. You commit your code and the test runs on the build server. The server says the test failed on step 5 with a StaleElementException. You locate the server, kick it, fill it with gasoline, and light it on fire because the builder server is clearly flawed and must be eliminated.

What happened? When you asked for 'EditDialog.LikeBox', Selenium ran across processes to the web page and grabbed the first thing that looked like the object you were asking for. Unfortunately, the thing it found wasn't the Like box for Hooty. It was the Like box for Boaty. That Like box is about to be destroyed by JavaScript and replaced by the Like box for Hooty. However, just for a millisecond, Boaty's Like box still exists and Selenium thinks that's the one you want. So it gives it to you. Your code continues along, not knowing that Selenium sold you an object reference that has had its odometer turned back by 100,000 miles and was in two accidents. Your code asks for the Text property thinking that it's been given a brand new object with leather trim and a working radio. As soon as Selenium runs back across the process divide to the web page, you are sad to find out that the text box no longer exists and Selenium never heard of this 'text' that you're referring to.

What should have happened? On the development side, it would be great if the developers would destroy Boaty's Like box when the Close button was clicked rather than after the edit button on Hooty is clicked. Alternately, Selenium could help us out by reducing the number of trips it takes across the inter-process divide. In the previous paragraph, you'll note that Selenium had to run out to the web page twice - once to get the object we're interested in and a second time to get the text off it. Optimally, it would ask once to "find a thing like this and get the text off it." You might still end up with the wrong text value ("penguins" instead of "whales"), but, at least you have some page data to tell you what might be going on rather than a useless exception.

Here's how we have gotten around this Selenium short-coming. The idea is to do essentially what I described above and make sure we both ask for the object and get the text property in one go.

In the last blog post, I showed how to return SafeElement so that we can add nice things to SafeElement. I'm going to build on that below:

public class SafeElement : IWebElement
{
    // The code I showed you last time

    public string Text // For example
    {
        get { return SafeFunction(e => e.Text); }
    }

    private void SafeAction(Action<IWebElement> a)
    {
        for (var i = 0; i < 4; i++) // You might use a configurable number of retries to fine-tune things
        {
            try
            {
                Thread.Sleep(500); // Good to make this configurable to fine-tune your tests
                a(GetActualContext(Context).FindElement(By));
                return;
            }
            catch (WebDriverException) { }
            Wait.Short();
        }
        a(GetActualContext(Context).FindElement(By));
    }

    private T SafeFunction<T>(Func<IWebElement, T> f)
    {
        for (var i = 0; i < 4; i++)
        {
            try
            {
                Thread.Sleep(500);
                return f(GetActualContext(Context).FindElement(By));
            }
            catch (WebDriverException) { }
            Wait.Short();
        }
        return f(GetActualContext(Context).FindElement(By));
    }

    private static ISearchContext GetActualContext(ISearchContext context)
    {
        var c = context as SafeElement;
        return c == null ? context : GetActualContext(c._driver).FindElement(c._by);
    }
}

Looks like a bunch of nonsense, right? It is nonsense. Nonsense that I have to do all this extra work just to make sure my tests don't throw an exception that should never happen in the first place.

The end result isn't so bad though. All we're really doing is looping over the request to 'get an object that looks like this and give me its property in one go' until Selenium decides to stop lying about what's on the page.

In my experience, these changes have gone a long way to making Selenium usable. At all.

Comments

Popular posts from this blog

A Cold Day in an SUV

Selenium Would Be Awesome If I Didn't Have to Design Around It

Your Automated System Tests Should Be a Joy to Write, Part 3