Germanium Abstract Selectors
Hi, this is Christopher. My first blog, ever, I promise. We will show the present state of the Selenium Page Object Pattern (in Java) and then tear it apart with a simpler Germanium approach that is equivalent but far superior. Tighten your belts: we have work to do.
In this blog we will go a bit into depth on how to create reusable abstract selectors in Germanium. We will choose an approach that closely resembles the WebDriver Page Object pattern on the surface (in the UI any web app is presenting), however the implementation in Germanium is, obviously, completely different. More importantly: it’s much easier and slicker with Germanium.
It is assumed that you are familiar with using Devtools in browsers such as IE11 and Chrome, you know some Python 3, and have experience using the API of Germanium to a simple level.
Let’s start with our sample web app which looks as shown below in Chrome, as we perform a short sequence of actions. We will then show, using Devtools in Chrome, how to find the simplest Xpath or Css selector strings that we can use in Germanium Selectors that we implement. You‘ll see snippets in Python of these selectors. Then we will show how to use them in Germanium to perform the actions that we illustrated at the start of this discussion. In this process, we will be demonstrating what I call the ‘box’ technique of decomposing a web page into separate portions, each of which one can use to perform WebDriver actions on, as if a human user did them.
Our goal will be to formulate these actions abstractly, thereby making them easier to understand, and very importantly: to also hide those low-level mouse clicks, mouse pointer selections, and mouse pointer movements, which no one wants to follow in detail, yet Selenium must have, in order to function during browser-user-interaction simulation.
This, as some of you already know, is the Germanium equivalent of the Selenium 3 Page Object Pattern. You will likely agree that with Germanium, the approach is much easier to develop and to maintain than the heavy class-based Page Object implementation, whether in Java or Python. No page factories, no fluent page objects. Just functions using the Germanium API and some abstractions to hide low-level details.
We must keep constantly in mind, however, that Selenium’s low-level actions implemented with the WebDriver API are absolutely essential for Selenium to painstakingly tell the browser exactly what to do. The Germanium API faithfully and compliantly uses the Selenium API. The results obtained by writing tests in sophisticated fashion with the Germanium API will convincingly demonstrate the usefulness, the effectiveness, the power, and finally the simplicity of the Germanium approach to Web app automation.
So let’s start the screenshots of our web app:
After clicking the pseudo button, above, in the red box at the left, we get:
This shows the expanded Navigation Bar (Navibar) which is where all our
interactions will occur. Another click on
int_IP_1 label above and we get
another kind of expansion downwards:
Then a click on the
A_1 shown above results in its insertion into the
document (all document details are irrelevant to our guided tour here; we are
focusing on Germanium controlled WebDriver interaction with a web page):
and after a click on the
int_IP_2 shown above gives:
We need to find, using the Germanium Python API, the locations of each clicking-point for each click we did above. I’ve drawn red boxes for them in the screenshots.
So how do we approach this with Germanium? We use the following technique:
Abstract Selector Strategy
Germanium Selectors are objects used as handles for Germanium locators to identify a part of a UI widget, usually an HTML element.
The locator 'gets' the element and the selector 'remembers' where it can be found, as long as the selector is valid. In most cases the selector uses a fairly permanent structural portion of the web page.
Germanium provides extensive flexibility for constructing selectors, making the selectors resistant to minor changes in a page’s structure, which means the selectors will still work after the changes to the page.
Of course, Germanium can’t perform miracles when a developer changes page structure too much. But common sense suggests the developer should design web pages sensibly with test automation in the back of his mind. This is where Germanium shines with resilient selectors and cooperative web developers who are aware the brittleness problem in web test automation.
We want descriptive names for UI areas displayed by our web app in the browser. We can do this by using implementations of the GE Abstract Selector, as many as we want, for each UI area we want to access. This is very broadly applicable in Germanium, which is very good for designing easily maintainable test automation. It allows us to design selectors for covering any distinct area that is visible (or becomes visible at some point) on a page: A button. A standard form widget (input text, text area, check-box, drop-down (or -up?) item list, radio button). A table. A specialized application specific custom element (you’ll see later as a Navibar, a Building Block, and an Insertion Point). A region.
Germanium provides an Element selector class, and I claim that it allows you to partition a web page any way you want, as long as there is an element with a selector for which the Element serves as that element’s bounding box. So every single element mentioned previously in this paragraph can have a Element containing it.
The only requirement is that the element can be inspected using a browser’s Devtools (all browsers have this tool and we get to that topic later). Let’s remember, then, that just as a complex Page Object test implementation can partition a web page into many smaller areas of related functionality (Think: page object composed of multiple sub-page objects with each sub-page object having several methods), we can do the same thing with a collection of Element class objects. An editor toolbar divided into groups of related buttons: Germanium can use the ‘boxing’ technique with Elements in an equivalent way that the Page Object pattern can be used.
For this blog, we just want a very simple concept of ‘boxing-in’ interesting rectangular areas to delineate elements for which we will implement Abstract Selectors as concrete instances with high-level not too abstract names and then define functions (in Python) also abstractly-named so they are useful as descriptive actions almost in a DSL-like context and raise our consciousness above the low-level drudgery of
select(from-element, to-element) and so on of the Selenium API.
Proper discussion of how to replace Selenium Page Objects with equivalent Germanium techniques is planned for in another blog.
Let’s put a dividing line here, take a break, and then resume our discussion of Abstract Selector implementation.
Partition page, use Devtools to get the selector
Our first step is to divide the page into visual sub-parts of interest which are labeled by useful names. These are the UI areas our test will use.
The second step is to use Devtools of the browser to help us with finding specific CSS classes if we need them.
Devtools shows details of this HTML element = UI area in our concept. We need to choose the most convenient path to it AND to formulate a selector in a human readable format. So we choose the shortest syntax string AND the Selector type that goes with it: Element, Button, InputText, etc os in the worst case the GE XPath or CSS Selector.
Finally we bind our selector syntax string to a specific function that we write as ‘implementation’ of the GE Abstract Selector.
For the Devtools step, take a look at the following screenshots keeping in mind those red boxes we had earlier. For each one, the steps we do are:
Use Devtools to analyze the element HTML to get a path to it
Choose the simplest GE Selector: Element, Button, Input, or string syntax (CSS or XPath)
Create the Selector using the Germanium API.
Notice that EVERY automation tool that uses the WebDriver approach has to do the first two steps, without exception.
The big payoff Germanium gives to you is in the third step. We make it real easy for you to program because we thought many hours about just how to do that for you, with our API.
Our API layer above the WebDriver one lets you abstract away the low-level mouse pointer movements and clicks so your tests are highly readable and maintainable.
There’s even more: we support behavior driven tests (using Behave variants for Python and Java) which REALLY go in the direction of a DSL dialect. But this is also a topic for a later blog.
Let’s get on with some Devtools screenshots:
We’ll create a Germanium Element Selector that uses a css class match like
div.navibar-component-handle-handle in an
Css syntax. Now we need the next
So we need a Germanium Selector with a string like
in xpath or css syntax. Then after clicking on it:
We need a selector with string like
'navibar-component-available-building-block' in xpath or css syntax, which
contains a string
'A_1'. Clicking on that selector, we get:
So then we need a selector with string like
'navibar-insertion-point-inside-chosen-bb' in xpath or css syntax, which contains the string
'int_IP_2'. Clicking on that selector, we get:
Let’s summmarize results so far:
Clickable Navibar pseudo-button area.
def NavibarHandle(): return Css('div.navibar-component-handle-handle')
Clickable Insertion Point, available inside navibar.
def NavibarAvailableInsertionPoint(name=None): if name: (1) return Element("div", css_classes=["navibar-insertion-point"]) \ .containing(Element("div", exact_text=name)) return Element("div", css_classes=["navibar-insertion-point"])
The optional parameter allows us to pick either a specific insertion point by name, or the full list of available insertion points if no name is given.
Clickable available Building Block inside navibar
def NavibarAvailableBuildingBlock(name=None): if name: return Element("div", css_classes=["navibar-component-available-building-block"])\ .containing(Element("div", exact_text=name)) return Css("div.navibar-component-available-building-block")
We can mix and match selectors.
Clickable Insertion Point contained in a chosen building block
def NavibarInsertionPointInChosen_BB(name=None): if name: return Element("div", contains_text=name, css_classes="navibar-insertion-point-inside-chosen-bb") return Element("div", css_classes="navibar-insertion-point-inside-chosen-bb")
We see above that the Selector column always contains the string from the Devtools column and that we followed the Abstract Selector pattern (modified as a function variation) when implementing the selector.
Also, in some selectors we supplied a name parameter to distinguish multiple matchings and end up with the desired unique selector.
Patching Everything Togethes
To conclude this blog, we show below a table with examples of actions using these selectors.
A Python test script written with these snippet examples will definitely work
in Chrome (where it was tested). A word of advice when using the Germanium Css
selector types (Element() with css_classes and Css()): there are browser
behavior differences for CSS that require a different syntax. Firefox for
example needs the textContent=’a_string’ attribute to find an element which
contains the string ‘a_string’. IE and Chrome however, work with the
innerText=’a_string’ attribute. Stay away from the contains() pseudo-class: it
is deprecated from CSS3 and may not work in browsers that do not support CSS
selectors natively. XPath is pretty much guaranteed to work in all major
browsers, but with Germanium you don’t even need to see it, just use
From these selectors:
We have this python code written in the actual test:
def i_expand_in_navibar_avail_ip(context, insertion_point_name): wait(NavibarAvailableInsertionPoint(insertion_point_name)) click(NavibarAvailableInsertionPoint(insertion_point_name)) return def i_add_the_avail_bb(context, building_block_name): wait(NavibarAvailableBuildingBlock(building_block_name)) click(NavibarAvailableBuildingBlock(building_block_name)) return def i_expand_in_navibar_ip_in_chosen_bb(context, insertion_point_name): wait(NavibarInsertionPointInChosen_BB(insertion_point_name)) click(NavibarInsertionPointInChosen_BB(insertion_point_name)) return
Do you see the beginnings of a DSL syntax resemblance? I do!
i_expand_in_navibar_avail_ip( ) i_add_the_avail_bb() i_expand_in_navibar_ip_in_chosen_bb()
Sure, we can refactor to get it even better. But, this blog must end. Now.