DEV Community

Cover image for Selectors and locators
Tymoteusz Stępień
Tymoteusz Stępień

Posted on

Selectors and locators

I must warn you that the piece that follows may not be the most content-rich you've ever read. Instead, it's more of a jumble of scattered notes that I'm leaving for my future self. Nonetheless, if you find something useful in these reflections, I'll consider this "project" a success.

Navigating through elements in browser's DOM while writing Cypress automated tests can be quite a challenging task, especially when it comes to selecting elements. Let me ask you something. How many times did you see, or even wrote by yourself (myself included!) such code?

Brittle test code

Something looks odd, isn't? For me it's lacking a proper and immutable way of selecting elements.

Why we should even care so much of "immutable way" of selecting elements? We just "clicking through" them, isn't? Just write and leave tactic.

Well... no, not really. Let me tell you why we should care:

  • Test stability: The goal of testing is to guarantee that the application is working properly. If the tests aren't stable, meaning they pass and fail without any modifications to the code, they aren't serving their objective. Utilizing immutable attributes ensures that the selector used to find the element in the DOM remains consistent and unchanged and it's independent of the element's content, even if the application's implementation changes.
  • Test maintainability: Tests aren't a one-time activity, they need to be maintained as the application evolves. If the selectors used to find elements in the DOM aren't stable, then they will need to be updated every time the implementation of the application changes. Using immutable attributes ensures that selectors remain stable and tests don't need as much changes as with "quick selects of elements".
  • Readability and clarity: Using attributes to pick elements in the DOM improves the readability and clarity of the test code. It explicitly states that the element is being chosen for testing reasons only, making the test's intent more clear.
  • Separation of objectives: Separating testing objectives from application implementation specifics is critical for test maintainability and readability. Using properties to identify DOM components allows for this separation of responsibilities and makes the tests less dependent on implementation details.

Also, it's just a good and well known standard.

Ok... so I should just toss into implementation of component just test attribute, like data-test-id with unique name for it, which will describe it?

Yes!

I have unit tests already, picking elements in them by someTestId attribute. Should I change it to data-test-id?

No need! You can reuse one pattern across all test levels which operates on app's DOM! I'll just use data-test-id in this example.

So what I'd change to above code? Where's area to expand our setup? Let's say I have 🪄 a magical powers 🪄. This is our React app "so far". Nothing fancy, just list of movies categories.

Simple categories app

First of all - each Link is part of the bigger list of items. So I'd change implementation, and I'd add to all of these links attribute, describing what is the purpose of them.

Added attributes

There might be questions:

Hey, should I keep attribute locator hardcoded like that? Also why not use something like link.id to create a "uniquely unique" element for each link? That way, I could easily find the "Action" button I'm looking for in provided example above. What do you think?

Those are great questions! Let's start with the second one. While using link.id (or any other unique way of describing attribute) to create a unique test attribute might seem like a good idea, it's important to remember that test attributes should reflect the purpose of the elements, not the details of a particular part of the app. For this purpose we write tests. By creating a test attribute that's tied to the implementation details of the app, you run the risk of making your tests more brittle and harder to maintain over time.

Regarding first question, the answer is actually quite simple: you can hardcode your test attributes, but it's generally more beneficial to keep them separate and avoid hardcoding whenever possible. That's where a locators file comes in handy. By maintaining a separate file for your test attributes, you can easily manage them in one place and avoid cluttering your code with hardcoded values.

So, let's create a locators file and add it to our code! This will make our test attributes more maintainable and easier to manage in the long run.

Added locators to implementation code

With such separation we added way of maintaining all locators in one place. So if we ever want to add any other element to our app, we'll be having all locators in one place.

Ok, ok, but we're here for Cypress. Not React course. Where's Cypress in this?

After we edited our implementation, it's time for Cypress. So now, we can add locator of our choice, and pick it in our test.

Simple locator switch to new version in test

Damn! So now I have to write such abominations over and over? It had to be breeze and make it easier for me!

Don't worry. This is why we have Cypress Commands. Let's make this test suite to its final form. Ah, also we should split this contains method. Because as mentioned above - we shouldn't keep implementation details with testing objectives. We should check if it's an Action text and not base on it.

Finished, polished test

Here we have it. We used custom command, which we can use across all of the tests, what's more - we have the same, non hardcoded locator to select proper element. As extra style points we did separation of objectives within the code, so now we test stuff, and not rely on them to test stuff.

Top comments (0)