Something Better than IDs for Identifying Elements in Selenium Tests

Developing Selenium / WebDriver tests is typically a cumbersome, back-and-forth process with developers. Conventional wisdom says you should use element IDs as much as possible.This requires that developers insert the element IDs into the code. In turn, developers ask for which elements do quality engineering (QE) needs IDs, to which QE responds with either some general, somewhat vague guidelines or a detailed list of elements. Despite the best efforts of both parties, elements are inevitably missed.

When I joined a newly formed  team on a recent project, we took a fresh look at how we could improve on this process to make it easier for all parties, cut down on errors, and improve our ability to test.

One member suggested that we use a special attribute as an identifier. He explained that his old group had great experiences using that approach. Now, after one year of practice with this method, I wholeheartedly agree.

Here is what our team agreed upon:

  1. Quality Engineering (QE) test writers will insert a special attribute, called it “data-qe-id,” as needed in the code.
  2. Developers refactoring code agreed to maintain  those attributes in the code.

Using this process, QE is placing IDs exactly where we need them, and developers are no longer burdened with having to guess where to put them.

Implementation

Obviously, the test writers need to have access to product code and be able to change privileges whenever necessary. Furthermore, it is important that test writers are able to stand up the application/service on their own machine. That way, the new attribute can be tested and test development can proceed.

QE test writers can adjust existing “data-qe-id”s without impacting product code, while product developers do not have to worry about breaking test code when changing IDs.

Reading the product code to find out where to put the attributes, the QE team members develop a familiarity with the product code which makes for better testing.

While adding attributes is low risk, we often ask developers for a code review. That way, developers are aware that their code changed, goof-ups are avoided, and developers get a better sense of what QE needs and perhaps whether or not  the attribute can be added proactively by the software developers.

Attribute Values

Here are some guidelines for choosing attribute values:

  1. Uniqueness

Each element must be uniquely identifiable. There are at least two ways to achieve that:

a. The identifier on each element in a page is unique.

This is the scheme we have used so far and have good experiences with. Below is a contrived example for credit card payment. Each element can uniquely be with a simple selector, e.g. to select the Visa option element use *[data-qe-id=’pay-card-visa’]

<html>
 <div data-qe-id="payment">
 <div data-qe-id="payment-card">
 <b>Credit Card</b>
 <br data-qe-id="payment-card-type">Card Type</br>
 <select>
 <option data-qe-id="payment-card-type-visa">Visa</option>
 <option data-qe-id="payment-card-type-amex">Amex</option>
 </select>
 <br>Expiration Year</br>
 <select>
 <option data-qe-id="payment-card-exp-year-2014">2014</option>
 <option data-qe-id="payment-card-exp-year-2015">2015</option>
 </select>
 <br>Expiration Month</br>
 <select>
 <option data-qe-id="payment-card-exp-month-jan">Jan</option>
 <option data-qe-id="payment-card-exp-month-feb">Feb</option>
 </select>
 </div>
 </div>
 <div data-qe-id="payment-check">
 <b>Check</b>
 </div>
 <div data-qe-id="payment-automatic">
 <b>Direct Debit Authorization</b>
 </div>
 <html>

b. Use composite selectors.

If pages consist of combinations of reusable components, it may be handy to identify only each component uniquely. In the “vacation” planner example below, the date selection for start and end date is a reused date component. To select a particular date uniquely, a composite selector must be used, e.g. to select February use *[data-qe-id=’start-date’] *[data-qe-id=’feb’]

<html>
 <div data-qe-id="start-date">
 <br>Start Date</br>
 <span>Month</span>
 <select>
 <option data-qe-id="jan">Jan</option>
 <option data-qe-id="feb">Feb</option>
 <option data-qe-id="mar">Mar</option>
 </select>
 <span>Day</span>
 <select>
 <option data-qe-id="1">1</option>
 <option data-qe-id="2">2</option>
 <option data-qe-id="3">3</option>
 </select>
 </div>
 <div data-qe-id="end-date">
 <br>End Date</br>
 <span>Month</span>
 <select>
 <option data-qe-id="jan">Jan</option>
 <option data-qe-id="feb">Feb</option>
 <option data-qe-id="mar">Mar</option>
 </select>
 <span>Day</span>
 <select>
 <option data-qe-id="1">1</option>
 <option data-qe-id="2">2</option>
 <option data-qe-id="3">3</option>
 </select>
 </div>
<html>

2. Follow a hierarchical naming scheme

Adopt a hierarchical scheme for the attribute values from general to specific. This makes it easier to name elements uniquely, describe the DOM structure, picture the element for a selector in the DOM and count elements (see Selectors section).

In the payment example, the widget is about making payments. This is the most general identifier. The options for making payments are credit card, check and authorized withdrawal. It is fairly obvious that the attribute payment-card-exp-year-2014 refers to the expiration year for credit card payments.

Following a (hierarchical) scheme also facilitates the computation of selectors. For example, there is no need to hard code 31 selectors for the up to 31 days in month in a page object.

3. They should be descriptive

While attribute value p-c-e-14 (for Payment with a Credit card that Expires in 2014) may be short and concise it would be hard to picture what that refers to. The attribute value payment-card-expiration-year-2014 gives a better idea.

Selectors

Being able to specify an identifying attribute on each element makes the selectors very simple and fast. For robustness it is better to avoid specifying the type of element. If a link is turned into a button, the same selector will still work:

css=*[data-qe-id='xyz']

To count elements in a multi-level tree structure, adhere to the naming scheme above and use “-” as a separator. The following would count the credit card options in the payment example:

css=*[data-qe-id^=payment-card-type-']

The separator character is included to avoid accidental partial matches. For example:

css=*[data-qe-id^=payment-card-type']

will also match

<br data-qe-id="payment-card-type">Card Type</br>

Conclusion

Using a particular attribute to identify elements in Selenium tests is pretty straight forward. The benefit is separating product development and testing concerns, which we found expedited test development, and improved the effectiveness of our testing.

How do you and your team ensure that you are testing all necessary elements in your Selenium tests? Have you found a way to improve test development, maintainability, and coverage? Please share your experiences in the comments section here!

Comments

  1. garrett says:

    Is using a unique attribute as fast as using an ID?

  2. Joe Loobeek says:

    Will this approach work for native mobile application code as well?

Leave a Comment