We will check out all the popular approaches that make it much easier to maintain and scale selectors for e2e tests.
A bit about selectors for e2e tests
When writing e2e tests, you must always use a selector. The more precise and unique it is, the better for you and your tests. Why? Suppose we have a selector that looks like this:
Now for e2e tests, suppose we are using Cypress, our code could look like this:
It's best to write selectors in such a way that they are always unique and independent of the HTML structure (changing it should not break the tests). Only such an approach will give you stability in tests. Of course, there may be exceptions to this rule, but if there is such a possibility, we should try to follow this pattern.
Honestly? That's an even worse idea. Why?
- When the component name change - you need to rename the class
- A selector is unique at the component scale, but not at the application scale :)
Maybe we should change from class to #id? Same problems will appear. Well then, what are we supposed to do? The problem is not what attribute we set, but what value we assign to it. We should know what the set of possible selectors is, where to get them and whether they are unique on the scale of the application/library or functionality.
How to achieve a finite number of selectors?
Now we know what number of selectors we have that should be used in e2e tests - we know that they are finite. Also, maintaining them will be much easier (change in one place).
I still see two problems here:
- This object can be terribly large - which will affect the size of the application,
- Anyone can use any attribute: class, id or other,
How to get rid of a large object and slim down the application?
First, let's define string literals from TypeScript, which will determine what selectors we can use.
Now in both e2e tests code and application code, it will use the following function:
A function that all it does is take a parameter and return it? LOL... Okay, now watch this. Instead of a large object, you have a function and type literals that always specify a finite number of selectors. The following function must be imported and called in the application and tests.
Pay attention to the hints you get from TypeScript
Creating a facade on Cypress.get
Well, okay, but still anyone can use any attribute they want. One developer will use classes, another id, and still another will use accessibility selectors that will give disabled people the ability to use the site.
We should use only specific attributes (a finite number) - for the purpose of the example it will be a data-attribute named data-i. There can be as many of them as we want, but the number must be finite and defined in one place.
Note that we are passing a Cypress instance and actually returning it, but with a captured type.
And this is how it looks like in the code of tests and applications.
Choosing selectors for e2e tests
Okay, but what about good practices? Some developers believe that selectors for e2e tests should be based on accessibility attributes where possible. It depends... I'm already explaining what's going on.
If someone wants to test the accessibility and business paths of the application in e2e tests at the same time, he must take into account that such tests can fail much more often and require changes. The use of data-attributes allows you to mark an HTML element with a unique value that refers to what this element is in a business context, and this attribute will only be used by e2e tests.
Thanks to this, changing styles, logic, adding other attributes, or changing the HTML structure itself (changing the order, adding an element higher or lower, and nesting) - will not break our tests.
The Cypress documentation itself says that using data-cy is the best practice. Our selectors will be completely separated from changes in styles or logic. I do not discourage the use of accessibility selectors, but it should be taken into account that such e2e tests will be much harder to maintain, and changes in the HTML structure will break the tests, and they shouldn't.
In my opinion, this is a perfect example of choosing a solution based on the developer's preferences, not profits/losses. I choose the data-attributes option because (and I'm not telling you it's best practice):
- E2e testing is time-consuming and expensive. Every false negative wastes our time. That's why I prefer to limit the number of failing tests when changing the HTML structure.
- I prefer to test accessibility in isolation - at the level of component unit tests, or even write e2e tests only for the component and check accessibility.
- Data-attributes tests are very easy to maintain and define a finite number of selectors.
Link to the Cypress documentation with the example I mentioned: Cypress best practices.
Scaling of selectors and type definitions
One function to handle all selectors is a bit much. To better scale the hints that TypeScript will offer us, we can create a few functions (per feature):
Summary and a few words about e2e tests
E2e tests are hellishly time-consuming. They must be written in such a way that they are resistant to changes in implementation - styles, logic, and structure of the HTML document. Using selectors based on data-attributes provides resistance to all three listed.
Did you change the class name? Did you change the HTML structure? Have you added new items? The tests will continue to work properly and do not require any changes, as long as the functionality actually works and the data-attribute selector has not changed.
Please treat this post as a curiosity. I really don't like to call things good/bad practice, because I think if someone can argue a solution, that's good enough to use it. Of course, if these arguments appeal to you.