Conventional wisdom regarding UI testing holds that you should always strive to select UI objects by the attribute that is least likely to change. With web UIs, you’ll typically see a hierarchy of preferred selectors as the following:
- Element ID
- Element name
- CSS class(es) associated with the element
- Text associated with the element
If you used this strategy with cucumber, you would get tests that looked something like this:
Given I go to the login page And I enter "johndoe" in the field with id "username" And I enter "password1" in the field with id "password" Then the input element with type "submit" should become enabled And I click the input element with type "submit"
But such steps are counter to the principles of behavior-driven development. The primary purpose of cucumber is that it allows your business owner to write requirements that can also directly serve as tests, and identifiers such as element ID or name are artifacts of implementation details, not part of the basic requirements. Or more simply–your business owner should be able to describe interaction with the application using only what’s visible to them in the UI.
More typically, cucumber steps for the login scenario above would look something like this:
Given I go to the login page And I enter "johndoe" in the "Username" field And I enter "password1" in the "Password" field The the "Log in" button should become enabled And I click the "Log in" button
Note that the steps use the visible text associated with each element to select the element.
The argument against this approach is: “But whenever your UI text changes–and let’s face it, it often changes–you’ll have to go update every single test!” While this is true, my rebuttal is that it’s a simple global search and replace change, and more importantly, it’s not a change to the code behind the cucumber, only to the cucumber test definitions, which, in my opinion, is a lower risk change than an actual code change.
To be fair, as the number of tests grew, I saw the redundancy between tests as a maintenance risk, so I’ve abstracted out frequently used functions, such as login and navigation, into single steps. Steps like the examples above no longer exists in our codebase. Instead, we have single steps such as:
Given I log in as username "johndoe" and password "password1"
So, now, if the text on our login page changes, we only have to change it in one place.
In my next blog post, I’ll show some of the code behind these steps.