Good and bad locators
The same order summary twice: once built for testing, once without. Switch to version 2 to see which locators survive a redesign.
A test finds an element by its locator. If the locator depends on things that change — the layout, the text, generated class names — the test breaks with the next release, even though nothing is wrong with the application. Map both sections below, run your tests, then switch to version 2.
A. Built for testing
An id on every single element, data-test on the ones that repeat.
Order #1042
PaidJohn Smith · john.smith@example.com
| Item | Qty | Price |
|---|---|---|
| Coffee beans, 1 kg | 2 | €24.00 |
| Ceramic mug | 1 | €9.50 |
| Paper filters | 3 | €6.00 |
Total €39.50
B. Built without testing in mind
Generated class names, nested divs, text only, and an id that changes on every load.
Locators, from the most to the least stable
| Locator | Why |
|---|---|
id, data-test | Made for finding the element; a redesign leaves them alone. data-test where an id does not fit, e.g. rows of a list. |
name, label, ARIA (role, aria-label) | Part of the form and of accessibility, so they change rarely. |
| CSS class | Made for the look; a new design changes it. |
| Text | Gets reworded and translated, and is rarely unique: both sections above have a "Cancel" button, and a locator by text finds the first one. |
Position, absolute XPath (/html/body/div[2]/…) | Breaks when anything above or around the element moves. |
Generated id or class (input-3f9a1c, css-1x9f3k) | Changes with every build, or even with every page load. |
What to practise
- Map section A: the order, the customer, the items, the total, the note and the two buttons. Write a test that saves a note and cancels the order.
- Map section B the same way, with whatever locators you can find, and run the same test.
- Switch to version 2 and run both again: A still passes; see which locators of B break, and why.
- Reload the page and look at the id of B's note field. Then find the best locator B allows (an ARIA role, a label, text inside a row) — and notice that B cannot be made stable without changing the application. Ask the developers for an
idor adata-test.
Page objects: guide, section 1.
Tested in VAF-Demo
A test of this page from VAF-Demo: the whole VAF chain, from the page to the element, is in the test.
[TestCase("/practice/mapping")]
[TestCase("/practice/mapping?version=2")]
public void StableLocatorsSurviveTheRedesign(string path)
{
SharedContext.LetsOpen(path);
SharedContext.App.Should().HaveSection<MappingPage.Order>()
.That.Should().HaveElement<MappingPage.Order.Total>()
.That.AsLabelText().Should().HaveValue("€39.50")
.AndAlso().Should().HaveElement<MappingPage.Order.Note>()
.That.AsTextInputField().LetsInsert("Ring twice")
.AndAlso().Should().HaveElement<MappingPage.Order.SaveNote>()
.That.AsButton().LetsClick()
.AndAlso().Should().HaveElement<MappingPage.Order.Cancel>()
.That.AsButton().LetsClick()
.AndAlso().Should().HaveElement<MappingPage.Order.Status>()
.That.AsLabelText().Should().HaveValue("Cancelled");
}
On GitHub (opens in a new tab)
-
Page object
—
MappingPage.cs: the locators of this page -
Fragile page object
—
MappingPageFragile.cs: the same screen mapped the way not to, with the reason on every locator -
Native tests
—
MappingTests.cs: the whole VAF chain in each test - The whole project and its README Latest test run (GitHub Actions)