Tuesday, July 1, 2014

Elementium: Browser testing done right

Browser testing is something that most large web applications have to go through. Most small projects start out with manual testing (i.e. you deploy and then click around the app to make sure nothing is broken) and then progress to automated testing that codifies these tests and automatically runs them on each commit (or you can start them manually before each commit/merge, if there is no continuous integration server setup). One of the major players in providing the framework necessary to perform automated testing is Selenium. More specifically, in the world of python, this is the Selenium python bindings. Note, this entire post, and elementium, is a python library. As such, if you are using a different language, elementium is not for you. Similarly, when I refer to "Selenium" I am referring specifically to the Selenium python bindings.

Everyone's reaction to Selenium is the same. First, it's a sense of awe for how it can navigate around a website, enter text, and click on buttons. This is soon followed by frustration when finding out that unlike most code that has been written to develop the application, testing in browsers is particularly fickle; widgets don't load in the same number of seconds in different runs, and elements become stale when the DOM updates. This leads to a state of despair and generally results in brittle testing code that is littered with time.sleep() calls and with ugly retry logic. This was exactly my progression through the journey of automated browser testing. Fortunately, it didn't end there.

Before I continue, let me be very clear in saying that I am eternally grateful to the folks that created and maintain Selenium - I could not do any of the testing that I do without it. It is a fantastic library that does a fantastic job. It just happens to have been created in an era when dynamic pages littered with AJAX calls was not the norm. Any of this work or potential "criticisms" are meant to be taken with this in mind -- without Selenium, we wouldn't even be this far.

If one takes a look at what people try to accomplish with automated browser testing it is:
  • Select browser elements, and perform actions based on selected elements
  • Assert to insist that particular elements exist
That's about 90% (if not more) of the work. Unfortunately, the base Selenium library does a great job at this if the page is not changing. I.e. as long as you don't have a dynamically loading page, that has widgets appear and get populated asynchronously, the core Selenium driver will serve you well. However, if you have such a dynamic page (or a web application that is completely written in JavaScript using something like Backbone.js), you will (or have already) run into two major obstacles:
  1. Waiting for an element to appear on the page
  2. Handling StaleElementExceptions
The current state of the art solution to this problem (as proposed by the hosted testing service SauceLabs) is to use something called "spin" methods. For example, if you need to assert that a certain element exists on a page, you should use a spin_assertthat spins and waits until the element appears. Here is a modified version of what they present (but the basic idea is exactly the same):
def spin_assert_equal(element, assertion):
    for i in xrange(60):
        try:
            # Re-get the element from the page via the lambda
            # and assert they are equal
            assert element() = assertion
            return
        except Exception, e:
            pass
        sleep(1)
    # If we get here, give it one more try, or make it raise an
    # AssertionError
    assert element() = assertion

# Create a lambda that finds the element
element = lambda: selenium.find_element_by_id('foo').text

# Try the assertion
spin_assert(element, 'FOO')
For full details on their method, check out their post here.

Does the above method work? Of course! But let me ask you this. Which of the following do you think is easier to read, understand, and maintain?
# Option 1: Pure selenium
element = lambda: selenium.find_element_by_id('foo').text
spin_assert(element, 'FOO')

# Option 2: Elementium
elements.find('#foo').insist(lambda e: e.text() == 'FOO')
I'm hoping that you say Option 2. Taking cues from jQuery, Elementium allows you to chain commands and handles all of the automatic retrying for you. For example, you could do something like this if you really wanted to:
elements.\
    find('.foo').\
    filter(lambda e: e.text().startswith('a')).\
    until(lambda e: len(e) == 2).\
    foreach(lamba e: e.click())
This here will find all elements with the CSS class 'foo', filter it down to the elements that have text starting with 'a', insist that there are exactly 2 of them, and then click on them. Yes, all of that in 1 line of code. Oh, and did I mention that this handles all of the retry logic automatically for you? (Btw, you can use the click() method on a list of elements as well elements.find('.foo').click())

How about if you want to wait until there are exactly 3 elements of this type on the page (because, for example, you have made some AJAX calls that creates three notifications)
elements.find('.notification').until(lambda e: len(e) == 3)
That's it. This will retry (for 20 seconds by default) and wait until there are three elements with the CSS class 'nofitication'.

"How does it do all this magic," you ask. I won't go into too much detail here, but under the hood, each selector (e.g. '.foo' or '#foo') is stored as a callback function (similar to the lambda: selenium.find_element_by_id('foo') of the first example. This way, when any of the calls to any of the methods of an element has an expected error (StaleElementException, etc.) it will recall this function. If you perform chaining, this will actually propagate that refresh (called update()) up the entire chain to ensure that all parts of the call are valid. Cool! Ok, and now to a "full" example that shows you exactly how to set everything up so that you can start using this today.
from selenium import webdriver
from elementium.drivers.se import SeElements

# Initialize the elements wrapper 
elements = SeElements(webdriver.Firefox())

# Do cool stuff
elements.find('#foo')
It's as simple as that!

Although this library is still under development, you can get this library and make use of it now by getting it from this public GitHub repo: https://github.com/actmd/elementium. There you will find more usage examples, the code, etc. We have been using it at ACT.md for the past 3 months and it has reduced our testing code, made it more stable, and made it much more legible. I'd say that's a win, win, win situation!

Don't hesitate to let me know if you have any questions, suggestions, etc. Happy testing.

9 comments:

  1. Can you show some complete and working example of Elementium and Behave, esepcialy with page objects?

    ReplyDelete
    Replies
    1. Sure -- thanks for the suggestion. I'll whip something up in the next week or so. I'll be sure to update the post with it and have it as part of the project under tutorials in the github repo.

      Delete
    2. I've put together a simple Behave example and it's now posted under examples/ in the GitHub repo. I didn't include the PageObject pattern as not everyone uses it. However, it should be fairly trivial to modify the example provided by Selenium (http://selenium-python.readthedocs.org/en/latest/page-objects.html) to pass in the Elementium object instead of the driver.

      (Note, you may have to switch to the develop branch in the repo as the next release isn't finalized yet.)

      Delete
  2. Hi, he tried to say that you can implement elementium with python only but you can test any webapp with it :)

    ReplyDelete
  3. Hi Patrick, does elementium works with IE browser?
    Thanks

    ReplyDelete
    Replies
    1. As Elementium is a wrapper around Selenium, it should work with any browser that Selenium has a driver for. I haven’t tested this, but here is the link to the IE driver: https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver once you have that downloaded and setup, the rest should be pretty much the same as above. Hope that helps!

      Delete
    2. Thanks for your quick response. I came from using splinter. As you know, it is a wrapper too, but a bit different.
      They wrap also how the driver is called, and they do not support IE. So to use IE I mush edit a splinter file and convert it to call IE. It may work but is not official, so any problem will not be taking in consideration.

      I see Elementium uses: "se = SeElements(webdriver.Firefox())", like inheriting directly from webdriver, pretty cool indeed :)

      Where is the best place to talk about elementium development/news?

      Delete
    3. Glad to hear that you are liking it!

      Great question regarding where to talk about Elementium. I just set up a google group forum for this sort of thing.

      https://groups.google.com/d/forum/elementiumlib

      Delete
  4. Hello, I am using Elementium with Python and behave. I am trying to close the browser after testing and it says "AttributeError: 'SeElements' object has no attribute 'close'". I tried with quit but it's not working either. How can I close the browser automatically?

    ReplyDelete