Automated, browser-based testing is a key element of web application development, benefiting both simple and complex applications. Writing effective tests for browser-based apps can be a complex, tedious and often repetitive task. In this post, I will be discussing a general approach to write meaningful, loosely-coupled UI tests for web applications by going beyond the Page Object Design Pattern into a more fine-grained approach I call ‘Logical entities to UI object mapping‘. I will show code in written Java 8 leveraging the Selenium and Selenide frameworks to show examples of the method described.
In web development, a common component used to perform browser-based testing is Selenium, which is a suite of frameworks and tools to automate browsers. All code in the Selenium projects is licensed under Apache 2.0. Selenium WebDriver is the most important component, which exposes an API to control several web browser and browser engines, and includes several language bindings: Java, C#, Python, Ruby, Perl, PHP and Javascript.
Page Object test design pattern
Selenium and the WebDriver API are very flexible and relatively easy to use. Make sure to check out the Test Design Consideration page of on the Selenium documentation for tips on how to structure your code, specially the Page Object Design Pattern section. There is a great post by Martin Fowler on the subject, plus lots of resources and blog posts on the web.
The following example is from the Selenium documentation, showing how coding a test using the straightforward WebDriver API can result in complex code that mixes many concerns:
/*** * Tests login feature */ public class Login { public void testLogin() { selenium.type("inputBox", "testUser"); selenium.type("password", "my supersecret password"); selenium.click("sign-in"); selenium.waitForPageToLoad("PageWaitPeriod"); Assert.assertTrue(selenium.isElementPresent("compose button"), "Login was unsuccessful"); } }
This kind of code, specially when adding lots of selectors and adding more test logic, can quickly transform into hard to maintain, spaguetti code.
When using the Page Object pattern, the original test code can be refactored into a more OOP class that has more semantics:
/** * Page Object encapsulates the Sign-in page. */ public class SignInPage { private Selenium selenium; public SignInPage(Selenium selenium) { this.selenium = selenium; if(!selenium.getTitle().equals("Sign in page")) { throw new IllegalStateException("This is not sign in page, current page is: " +selenium.getLocation()); } } /** * Login as valid user * * @param userName * @param password * @return HomePage object */ public HomePage loginValidUser(String userName, String password) { selenium.type("usernamefield", userName); selenium.type("passwordfield", password); selenium.click("sign-in"); selenium.waitForPageToLoad("waitPeriod"); return new HomePage(selenium); } }
There are also fair warnings on the tradeoffs involved in applying the pattern, as described in this blog post.
In this case the warning is focused on the risk of grouping unrelated semantic and intent in aggregation ‘page’ classes.
In my case, I also tend to stay away from ‘page’ classes and write UI abstractions around the application’s logical entities. Logical entities are mapped to UI objects and then accessed naturally through their relationships so user actions and navigation are kept intentional. To also keep code within the domain of UI testing, most UI class methods are explicitly called after common user browser actions like clicking, moving the mouse or entering text into forms. In practice I have found that this combination is a ‘sweet spot’ combination of useful abstraction, loose coupling, testability and practicality.
The following diagram illustrates the concept for a typical content management system three level master-detail:
I will be showing next some specific examples of applying the ‘Logical entities to UI object mapping’ pattern in Java 8, using Selenide on top of Selenium to automate the browser.
Introducing Selenide
Surprisingly less well-known than it should be, Selenide is a great Selenium WebDriver abstraction layer written in Java that makes it easier to manipulate browsers and write tests than just by using Selenium. Selenide simplifies writing tests based on Selenium with a surprisingly intuitive interface. To demonstrate the simplicity, here’s an example from the getting started:
@Test public void userCanLoginByUsername() { open("/login"); $(By.name("user.name")).setValue("johny"); $("#submit").click(); $(".loading_progress").should(disappear); // Waits until element disappears $("#username").shouldHave(text("Hello, Johny!")); // Waits until element gets text }
The interface offered is easy to use and you usually don’t even need to look at the documentation. Waiting for elements to appear is implicit in many parts of the code and Selenide generally works as you would expect. I encourage you to try it, write a few tests and you will be surprised at the simplicity.
Regardless of the added convenience and abstraction of the Selenide library, we can still end with spaguetti code if we are not careful. For instance, if we use CSS selectors to access one UI element with $(org.openqa.selenium.By seleniumSelector)
and want to change related classes in the client-side, we need to go everywhere in our testing code and change the CSS selector references.
Separating UI handling from tests
In a OSS project I’ve been recently working on (Morfeu), I am doing extensive testing of the web UI and I’m using the Logical entities to UI object mapping approach all over the place. For instance, in the project there is a list of catalogues (like a master-detail listing) as a logical application entity. Each catalogue has a list of different catalogue entries, which in turn contain documents (not shown in the example), completing a simple three layer of logical app entities. The code to operate and access the master catalogue list is as follows:
public class UICatalogues { private static final String CATALOGUE_LIST = "#catalogue-list"; private static final String CATALOGUE_LIST_ENTRY = ".catalogue-list-entry"; public static UICatalogues openCatalogues() { return new UICatalogues(); } public UICatalogues shouldAppear() { $(CATALOGUE_LIST).should(appear); return this; } public UICatalogues shouldBeVisible() { $(CATALOGUE_LIST).shouldBe(visible); return this; } public List allCatalogueEntries() { return $(CATALOGUE_LIST_ENTRY).stream().map(e -> new UICatalogueEntry(e)).collect(Collectors.toList()); } public UICatalogue clickOn(int i) { List catalogueEntries = this.allCatalogueEntries(); int count = catalogueEntries.size(); if (i>=count) { throw new IndexOutOfBoundsException("Could not click on catalogue entry "+i+" as there are only "+count); } catalogueEntries.get(i).click(); return UICatalogue.openCatalogue(); } }
To start with, public static UICatalogues openCatalogues()
is used to open the catalogues list, it’s a static method as one of the constraints is that there is only one catalogue list and that it appears as soon as the user loads the application. A static method is a convenient way to access the ‘catalogues’ object instance as opposed to a public constructor, which maps to the application semantics. If there was a need to change the behaviour into requiring user action to load the catalogues (such as a mouse click) the implementation could be changed without changing the caller code. If the change were to be quite radical and significantly change the logical entity relationships, like allowing multiple catalogues, the API and caller code could (and should) be refactored to reflect the big change in application behaviour.
The methods UICatalogues shouldAppear()
and shouldBeVisible()
are pretty self explanatory, and can be used to ensure the catalogue list loads and displays properly.
The methods are quite simple themselves and the usage of Selenide is pretty much self-explanatory.
The next method List allCatalogueEntries()
is used to obtain the list of catalogues, and takes advantage of Java 8 streams, mapping the list of found elements into new UICatalogueEntry
instances:
public List allCatalogueEntries() { return $(CATALOGUE_LIST_ENTRY).stream().map(e -> new UICatalogueEntry(e)).collect(Collectors.toList()); }
A stream of low-level catalogue entry elements found by Selenide is mapped to new catalogue instances and then collected into a using Java 8 list collector.
UICatalogueEntry
‘s constructor accepts a SelenideElement
to provide each catalogue entry instance with its local context. SelenideElement
instances are a wrapper of Selenium elements and are the basic building blocks of testing using Selenide.
The last method UICatalogue clickOn(int i)
is a convenience method to click on a specific catalogue entry and is also quite straightforward:
public UICatalogue clickOn(int i) { List catalogueEntries = this.allCatalogueEntries(); int count = catalogueEntries.size(); if (i>=count) { throw new IndexOutOfBoundsException("Could not click on catalogue entry "+i+" as there are only "+count); } catalogueEntries.get(i).click(); return UICatalogue.openCatalogue(); }
UICatalogue
is also a semantic web UI class that includes the click()
method that loads a catalogue, which is returned (in this case also without any parameters in the current implementation).
It should be noted that there are other valid ways to design this. The clickOn
method only uses public methods so it could rightly be perceived as ‘client’ code, but as clicking on a catalogue is done very often, this is offered as a way to avoid repetition. It’s important to spend some time thinking about this design, giving consideration to style, operation frequency, potential for repetition, amount of convenience methods and so forth.
Also a bit of a personal code style choice, I am staying away from get/set semantics to better distinguish UI test logic from typical application code (which commonly employs get/set prefixes). A backend code getter could potentially perform a complex operation while the UI code just selects a CSS class and reads a value displayed on the page, hence the shorter method name. Following this logic, user action methods like clickOnXXX
will be the ones performing complex operations like navigating to a different page and so forth, so they have a more explicit verb prefix. Of course getter/setters can be used if that suits more your style.
Using the abstraction UI classes
Usage is also straightforward, like this method:
@Test public void catalogueListTest() throws Exception { open(appBaseURL); List catalogueEntries = UICatalogues.openCatalogues() .shouldAppear() .getAllCatalogueEntries(); assertEquals(EXPECTED_CATALOGUES_COUNT, catalogueEntries.size()); assertEquals("Wrong catalogue content", "Catalogue 1", catalogueEntries.get(0).name()); assertEquals("Wrong catalogue content", "Catalogue 2", catalogueEntries.get(1).name()); assertEquals("Wrong catalogue content", "Catalogue 1 yaml", catalogueEntries.get(2).name()); UIProblem.shouldNotBeVisible(); }
This code is easy to read, and shows exactly what kind of behaviour the is being tested. In this case, it’s testing that once the application is loaded, the catalogues should appear, there should be EXPECTED_CATALOGUES_COUNT
entries, and also the particular catalogue order and names. Finally, no error should be shown. Also note the readability of catalogueEntries.get(0).name()
vs a more verbose catalogueEntries.get(0).getName()
.
More complex behaviour is also modelled quite well:
UICellEditor stuffEditor = stuff.edit().shouldAppear(); assertNotNull(stuffEditor); Optional value = stuffEditor.getValue(); assertTrue(value.isPresent()); assertEquals("Stuff content", value.get()); assertFalse("Should not be able to create a value for this cell", stuffEditor.isCreateValueVisible()); assertTrue("Should be able to remove value for this cell", stuffEditor.isRemoveValueVisible()); stuffEditor.clickRemoveValue(); value = stuffEditor.getValue(); assertFalse(value.isPresent());
In this case the code is testing a form that contains a value and can be removed entirely (as opposed to cleared) by clicking on a specific widget or button. Once the widget is activated, the form value disappears and is not there anymore.
More examples like these can be found in the Morfeu project tests.
Conclusions
It is definitely useful to abstract web UI testing using techniques like ‘Logical entities to UI object mapping’ described in this post or the Page Object mapping pattern. If the right abstraction level is applied correctly, test are more meaningful, their intent is more obvious and the actual web app implementation can be changed more easily. Techniques applied with tools like Selenide make writing the semantic UI code even easier and combined with Java 8 stream support, testing code ends up being super-fun to write.
Annex
For more examples please take a look at examples from the Selenide documentation on the direct UI and test mix approach:
@Test public void search_selenide_in_google() { open("https://google.com/ncr"); $(By.name("q")).val("selenide").pressEnter(); $("#ires .g").shouldHave(sizeGreaterThan(1)); $("#ires .g").shouldBe(visible).shouldHave( text("Selenide: concise UI tests in Java"), text("selenide.org")); }
(Note that the example is intended for simplicity and separation of concerns is not the aim of the code). The code is pretty readable and concise, but could be improved by applying the Page Object test design pattern or the one described in this blog post. Alternatively, for smaller tests having the classes in constants or in a test configuration could also be practical.
The Selenide project also has some examples of the more semantic approach using Page Objects here.