3 best practices for Selenium testing when constructing your page
Having spent now about two months building out Magium there are a couple of things that are worth sharing as I’ve been working through several different versions of Magento. These practices (best practices?) may or may not be “officialized” by the Overlords of the Internet but what I have found is that I have the most trouble when these practices are not executed.
The basic premise behind each of these is that the quickest path to the most specific element is best. Long Xpaths may be prone to break and do not lend themselves well to re-use. Specificity and predictability make building Selenium tests not quite a dream, but definitely not a nightmare.
Wrap ALL text in an HTML element – leave no orphaned text
Of all of these this is one of the things that has probably been the hardest to deal with. HTML is really good about allowing us to write semi-structured web pages. I believe that with many things, HTML and PHP included, “fail friendly” software helps increase usage. But that wreaks havok when you are trying to automate browser testing.
Consider the following code, which is not unusual:
<div class="product">
<a href="/product-one">
Product One
<em>3 left in stock</em>
</a>
</div>
Say that part your test is clicking on the product name so you can add it to the cart. What Xpath would you use that could be consistently used? Here are some Xpaths that will not work.
//a[.="Product One"]
will not match because the text of thea
element isProduct One 3 left in stock
//a[starts-with( @href , "Product One")
– “Product One Two” would also be matched//a[@href="/product-one"]"
might work, but you have to rewrite the test if the URL changes, such as moving between a dev and test environment.
How can you make this code easier to test? Wrap the product name in an HTML element
<div class="product">
<a href="/product-one">
<span>Product One</span>
<em>3 left in stock</em>
</a>
</div>
Now you can write the Xpath //a/span[.="Product One"]
and be sure that it will match. Though, truthfully, you will probably want to use //a/span[concat(" ",normalize-space(.)," ") = " Product One "]
instead. That allows you to normalize for incidental spaces caused by line feeds in the HTML.
That is actually what happens in Magium when you request an element byText()
. In the Grid Widget test the Edit button is clicked on.
$this->byText('Edit')->click();
What this does is build the Xpath //span[concat(" ",normalize-space(.)," ") = " Edit "]
.
If an element has function, identify it.
A good example of this is a Save button. There will often only be one Save button on a page (the Magento admin UI notwithstanding). As such, providing an ID is a useful thing to do.
<button id="save-button">
<span>
<span>Save</span>
</span>
</button>
Then you don’t have to worry about if the Xpath is //button[.=" Save "]
or //button[.="Save"]
. Do they look the same? Look again. They are verrry different.
There are, however, times when you will have more than one element. The Magento Admin UI will often have two. One for the main part of the page and then another for the fixed bar across the top when the main button scrolls out of view. The way I get around this is to scroll to the top of the page before clicking the Save button but that won’t always be possible.
You could also use WAI-ARIA role landmarks to help out as well. Roles such as menu, search, or heading are quite useful for categorizing the markup on the page. But note that these will likely still need additional Xpaths to find the specific element you are looking for. Searching for //li[@role="menuitem" and .="Product Name"]
may not get you what you want because it is possible that there are multiple menus with the item in it.
My preference is that anything that has a particular function has an ID on it. Even if the ID is convoluted. Convoluted, but predictable, is fine in my books. Unpredictable IDs, such as the Save buttons in the Magento 1 Admin UI are not very helpful.
Group data using classes and IDs
When there is like data on a page, such as a list of products, it is beneficial to identify a container that holds those elements. Sometimes it will be an ID and sometimes it will be a class. For example, Magento uses the class category-products to identify the container for ul
list of category products. Personally, I like IDs for that because it guarantees that when I query for it I am getting the one-true-element. If you really need to have multiple product lists, my opinion is that the following is best.
<div id="primary-product-list" class="category-products">
<ul>product list</ul>
</div>
<div id="secondary-product-list" class="category-products">
<ul>product list</ul>
</div>
Likewise, make sure that elements that fullfill the same role, such as a product title, have the same CSS class assigned to it. Magento 1 uses the product-name
class to identify a product’s name, which is also where the link usually resides.
Conlusion
While CSS is very important for building the user experience and making a good looking website it is also a critical component of automating your browser testing. Having a structured document that makes each element individually selectable also helps. Follow these 3 practices and you will find that your tests will become much easier to write.