loading...

Web UI tests reinforcement with webtau framework (Selenium based)

mykolagolubyev profile image MykolaGolubyev ・3 min read

One of the biggest problem with Web UI tests is their brittleness. Moving a page element around or replacing an input box with a drop down can cause a ripple effect that breaks dozens of your tests.

In this article I will show you how to encapsulate your UI implementation details so your tests can focus on testing features.

I will be using the webtau framework that has primitives designed to help with tests brittleness

First test attempt

Let’s test a search feature of an imaginary app.

scenario('search by specific query') {
    browser.open('/search')

    $('#search-box').setValue('search this')
    $('#search-box').sendKeys("\n")

    $('#results .result').count.shouldBe > 1
}

Test:

  • open a search page
  • set a value to an input box that can be located by search-box id
  • simulate enter key and check that there are some results

I claim that there should be only one reason to change this test and that is if the actual functionality of your search experience changes (Single Responsibility Principle anyone?).

However there are four non functional reasons that can force our test to change:

  • search page url changes
  • the way we initiate the search changes: enter key vs mouse click
  • ids/classes/attributes associated with elements are changed
  • server response time becomes slower

Let’s address them one by one.

Extract elements definition

In webtau $(css) creates an instance of PageElement that you use to simulate user actions and query values.

Created instances are lazy and can be defined before the browser is even opened.

def searchBox = $('#search-box')
def numberOfResults = searchBox.count

scenario('search by specific query') {
    browser.open('/search')

    searchBox.setValue('search this')
    searchBox.sendKeys("\n")

    numberOfResults.shouldBe > 1
}

This is a step in a right direction, but we still have exposed page url and hardcoded Enter key press.

Extract user actions

Let’s create a submit method to encapsulate the way users are supposed to initiate the search.

def searchBox = $('#search-box')
def numberOfResults = searchBox.count

scenario('search by specific query') {
    submit('search this')
    numberOfResults.shouldBe > 1
}

def submit(query) {
    browser.open("/search")

    searchBox.setValue(query)
    searchBox.sendKeys("\n")
}

I think the changes we made make the test easier to reason about. But I still want to give it a final touch: move definitions out of a test file so they can be potentially used by other test scenarios.

Encapsulation

Let’s move the action and page elements definitions to a separate class. You may have heard of the PageObject concept before.

In webtau page objects are simple class instances. It is the lazy nature of PageElement that makes this simplicity possible.

package pages

import static com.twosigma.webtau.WebTauDsl.*

class SearchPage {
    def searchBox = $('#search-box')
    def numberOfResults = $('#results .result').count

    def submit(query) {
        browser.open("/search")

        searchBox.setValue(query)
        searchBox.sendKeys("\n")
    }
}

It doesn’t matter when we create the instance of the class since searchBox and numberOfResults will be lazily initiated.

Let’s create the Pages class with static page object instances to make it convenient to access them from different scenarios.

package pages

class Pages {
    static final def search = new SearchPage()
    static final def calculation = new CalculationPage()
    static final def form = new FormPage()
}

Our page object is one import away.

import static pages.Pages.*

scenario("search by specific query") {
    search.submit("search this")
    search.numberOfResults.shouldBe > 1
}

Dealing with asynchronous brittleness

Our test is now reinforced and can sustain UI non-functional changes. But there is one unprotected area left uncovered.

During local development our server is blazingly fast and search results are produced in under 5ms. Weeks later we will run the tests in QA environment and most likely we will be dealing with the failed assertion.

Let’s do a final test reinforcement.

import static pages.Pages.*

scenario("search by specific query") {
    search.submit("search this")
    search.numberOfResults.waitToBe > 1
}

We replaced shouldBe with waitToBe. As a result, instead of failing the assertion right away waitTo will re-query numberOfResults multiple times until it times out (driven by configuration).

Groovy, Java and beyond

I have been using Groovy for testing for the last decade and I think it is perfect for the job. But if you are not a Groovy fan, you can use Java or other JVM languages.

If you are interested in Java or other JVM examples, please hit me in the comments and I will be happy to help.

Links

GitHub

Introduction to REST API testing with webtau

Discussion

markdown guide