Najkrótsza droga do stabilnych lokatorów w testach
- XPath najlepiej działa, gdy selektor musi oprzeć się na tekście, relacji lub atrybucie, który jest stabilny.
- W testach webowych najcenniejsze są krótkie, względne ścieżki oraz predykaty w nawiasach kwadratowych.
- Funkcje takie jak
contains(),starts-with()inormalize-space()robią większą różnicę niż długa lista symboli. - W Selenium i podobnych narzędziach XPath jest bardzo użyteczny, ale nie powinien być pierwszym wyborem, jeśli prosty CSS wystarczy.
XPath w testach automatycznych i kiedy naprawdę się przydaje
W przeglądarce XPath działa na drzewie DOM, czyli na strukturze elementów, które widzi narzędzie automatyzujące, a nie na “ładnym” kodzie źródłowym, który czasem oglądamy w devtools. Dzięki temu można opisać element nie tylko po klasie albo identyfikatorze, ale też po tekście, położeniu względem rodzica czy sąsiada. W praktyce używam go najczęściej wtedy, gdy aplikacja nie daje dobrych ID, a test ma nadal trafić w jeden konkretny element.
To narzędzie ma jednak sens tylko wtedy, gdy stosuje się je świadomie. Jeśli selektor ma opisywać całą drogę od korzenia dokumentu do przycisku, zwykle oznacza to, że test próbuje odtworzyć layout zamiast zachowania użytkownika. Żeby korzystać z XPath szybko i bez nerwów, trzeba znać kilka znaków i funkcji, które robią całą robotę.
Najważniejsze elementy składni, które warto mieć w głowie
Najpierw warto zapamiętać podstawowe klocki. Z pozoru XPath wygląda groźnie, ale w codziennej pracy najczęściej obraca się wokół kilku operatorów, dwóch typów ścieżek i garści funkcji filtrujących.
| Składnia | Co robi | Przykład | Kiedy używać |
|---|---|---|---|
/ |
Przechodzi o jeden poziom niżej od korzenia | /html/body/div |
Gdy świadomie adresujesz strukturę od początku |
// |
Szukает potomków na dowolnej głębokości | //button |
Gdy nie chcesz wiązać się z pełną ścieżką |
.// |
Szukает względnie wewnątrz bieżącego węzła | .//span |
Gdy zawężasz zakres do konkretnego komponentu |
@ |
Odwołuje się do atrybutu | //input[@name='email'] |
Przy selektorach po atrybutach |
[] |
Filtruje wynik przez predykat | //li[1] |
Gdy trzeba ograniczyć zbiór |
* |
Dopasowuje dowolny element | //form/* |
Gdy ważna jest struktura, a nie nazwa taga |
text() |
Wskazuje węzeł tekstowy | //button[text()='Zaloguj'] |
Przy dopasowaniu po treści |
contains() |
Sprawdza częściowe dopasowanie | //a[contains(@href,'login')] |
Gdy atrybut ma stały fragment |
starts-with() |
Sprawdza początek tekstu lub atrybutu | //div[starts-with(@id,'user_')] |
Gdy prefiks jest stały |
normalize-space() |
Usuwa zbędne spacje z tekstu | //button[normalize-space()='Wyślij'] |
Przy tekście z wcięciami lub łamaniami linii |
and / or |
Łączy warunki | //input[@type='text' and @name='email'] |
Gdy chcesz zawęzić dopasowanie |
To właśnie te elementy najczęściej decydują, czy locator będzie czytelny, czy zacznie przypominać przypadkową ścieżkę przez cały layout. Gdy podstawy są już jasne, największą różnicę robią osie i predykaty, bo to one pozwalają precyzyjnie zawężać wynik.
Osie i predykaty, które najczęściej ratują lokatory
Osie są mniej widowiskowe od funkcji, ale w testach często ratują sytuację. To one mówią, w którą stronę można przejść po drzewie DOM, żeby odwołać się do rodzica, rodzeństwa albo potomka bez budowania długiej, kruchej ścieżki.
Najpraktyczniejsze osie
| Oś | Co robi | Przykład | Po co się przydaje |
|---|---|---|---|
parent:: |
Przechodzi do rodzica | //input[@name='email']/parent::div |
Gdy struktura opiera się na wrapperze |
child:: |
Wybiera bezpośrednie dzieci | //ul[@id='menu']/child::li |
Gdy trzeba zawęzić tylko do bezpośrednich elementów |
descendant:: |
Wybiera wszystkich potomków | //form/descendant::input |
Gdy komponent ma wiele poziomów zagnieżdżenia |
following-sibling:: |
Wybiera następną siostrę | //label[normalize-space()='Email']/following-sibling::input |
Gdy etykieta i pole są obok siebie |
preceding-sibling:: |
Wybiera poprzednią siostrę | //input[@name='email']/preceding-sibling::label |
Gdy element pomocniczy stoi po drugiej stronie |
ancestor:: |
Idzie w górę do przodka | //span[text()='Błąd']/ancestor::form |
Gdy trzeba złapać cały kontener z komunikatem |
ancestor-or-self:: |
Idzie w górę razem z bieżącym węzłem | //div[@class='error']/ancestor-or-self::form |
Gdy chcesz objąć sam element i jego kontener |
self:: |
Odnosi się do bieżącego węzła | //button[self::button and @type='submit'] |
Rzadziej, ale bywa przy złożonych warunkach |
Predykat to po prostu filtr. Najważniejsze rozróżnienie, które lubię przypominać zespołom, dotyczy indeksowania: //li[1] nie znaczy dokładnie to samo co (//li)[1]. Pierwszy wariant wybiera pierwsze li w każdym dopasowanym rodzicu, a drugi pierwszy element z całego wyniku. Ten detal potrafi uratować albo zepsuć test.
-
(//tr)[last()]wybiera ostatni wiersz tabeli. -
//button[normalize-space()='Zaloguj']dobrze znosi dodatkowe spacje i łamanie linii. -
//input[contains(@placeholder,'Szukaj')]działa, gdy atrybut ma stały fragment, ale nie jest identyczny. -
//div[not(contains(@class,'disabled'))]pozwala odfiltrować stany nieaktywne. -
//li[position() <= 3]ogranicza wynik do kilku pierwszych elementów, jeśli kolejność jest stała.
Największą ostrożność zachowuję przy contains() na klasach, bo częściowe dopasowanie może złapać zbyt wiele elementów. To działa dobrze tylko wtedy, gdy fragment jest naprawdę unikalny i nie grozi przypadkowym trafieniem w podobną klasę. Z tych klocków składają się gotowe wzorce, których używam najczęściej.

Gotowe wzorce selektorów do najczęstszych przypadków
Jeśli chcesz pisać locatory szybciej, nie zaczynaj od budowania pełnej ścieżki. Lepiej od razu dobrać wzorzec do sytuacji: atrybut, tekst albo relację między elementami.
Gdy masz stabilny atrybut
//input[@data-testid='email']
//button[@type='submit']
//form[@id='login']//input[@type='password']
To mój pierwszy wybór, jeśli aplikacja udostępnia data-testid albo podobny atrybut testowy. Taki selektor jest zwykle krótki, czytelny i odporny na zmiany layoutu. Właśnie dlatego warto projektować aplikację tak, by dawała testom własne, stabilne haki zamiast zmuszać je do zgadywania struktury.
Gdy trzeba trafić po tekście
//button[normalize-space()='Zaloguj się']
//a[contains(normalize-space(), 'Regulamin')]
Tu najważniejsze jest normalize-space(), bo tekst w HTML bywa rozbity spacjami, nowymi liniami albo dodatkowymi wcięciami. Dopasowanie po tekście dobrze sprawdza się przy przyciskach, linkach i komunikatach, ale mniej lubię je przy elementach, których treść zmienia się wraz z językiem interfejsu. Jeśli aplikacja jest wielojęzyczna, taki selektor trzeba przemyśleć dwa razy.
Przeczytaj również: Oś przodków XPath - Odporne selektory w testach UI
Gdy element jest opisany przez sąsiada
//label[normalize-space()='E-mail']/following-sibling::input
//span[text()='Błąd']/ancestor::form
To podejście przydaje się wtedy, gdy interfejs ma sensowną strukturę, ale nie ma dobrego identyfikatora. W testach formularzy właśnie takie selektory często okazują się bardziej odporne niż długie ścieżki przez kolejne div. Najlepiej działa to tam, gdzie projekt UI jest spójny, a relacje między elementami są przewidywalne.
W praktyce często łączę kilka warunków: najpierw zawężam kontekst do formularza lub konkretnej sekcji, a dopiero potem sprawdzam atrybut albo tekst. Taki styl zwykle daje krótsze i bezpieczniejsze selektory niż rozrzucone po całym DOM //. To prowadzi do prostego pytania: kiedy XPath jest lepszy od CSS, a kiedy lepiej nie komplikować testu.
XPath czy CSS selektory w automatyzacji
Dokumentacja Selenium zwykle stawia prostsze selektory wyżej, jeśli da się nimi jednoznacznie wskazać element bez kombinowania. XPath wygrywa tam, gdzie potrzebujesz tekstu, relacji z rodzicem albo bardziej opisowego warunku. Ja traktuję CSS jako opcję domyślną, a XPath jako narzędzie do zadań specjalnych.
| Kryterium | CSS | XPath | Mój wybór |
|---|---|---|---|
| Proste ID i klasy | Bardzo dobry | Działa, ale zwykle jest nadmiarowy | CSS |
| Tekst elementu | Słabszy | Bardzo dobry | XPath |
| Relacja z rodzicem lub sąsiadem | Ograniczona | Bardzo dobra | XPath |
| Czytelność prostych selektorów | Zwykle lepsza | Dobra, ale rośnie złożoność | CSS |
| Złożone warunki | Umiarkowana | Bardzo dobra | XPath |
| Debugowanie i utrzymanie | Łatwiejsze przy prostych przypadkach | Czasem trudniejsze | Zależy od kontekstu |
Nie chodzi więc o to, żeby całkowicie unikać XPath. Chodzi o to, żeby nie używać go tam, gdzie prostszy selektor zrobi to samo szybciej do przeczytania i łatwiej do utrzymania. Im mniej sztucznej złożoności w locatorach, tym mniej niepotrzebnych awarii w testach.
Najczęstsze błędy, które psują selektory
Najwięcej problemów widzę nie w samym języku, tylko w sposobie myślenia o locatorach. Gdy selektor zaczyna być “sprytny”, zwykle po chwili staje się kruchy.
-
Absolutne ścieżki typu
/html/body/div[2]/div[1]/...wyglądają precyzyjnie, ale rozpadają się przy najmniejszej zmianie układu. - Indeksy jako główna strategia działają tylko wtedy, gdy kolejność elementów jest naprawdę stała. Jeśli lista się przestawia, test też zacznie się mylić.
-
Zbyt szerokie
//potrafi złapać więcej węzłów niż trzeba, a potem test trafia w pierwszy z brzegu element. -
Brak
normalize-space()powoduje, że pozornie poprawny tekst nie pasuje przez spacje, łamania linii albo formatowanie. -
Nieostrożne
contains()na klasach może dopasować podobne nazwy, których wcale nie chciałeś ruszać. - Brak zawężenia kontekstu sprawia, że selektor działa dziś, ale jutro zaczyna wskazywać element z innej sekcji strony.
Najuczciwszy test dla selektora jest prosty: czy dałoby się go zrozumieć po miesiącu bez wracania do całej struktury HTML? Jeśli odpowiedź brzmi “nie”, zwykle warto go skrócić albo oprzeć o stabilniejszy atrybut. I właśnie wtedy przydają się ostatnie zasady, które pomagają budować locatory odporne na refaktoryzację.
Jak pisać lokatory, które przeżyją refaktoryzację
Jeśli mam zostawić po sobie jedną praktyczną zasadę, to jest nią prostota z domieszką kontekstu. Najpierw szukam stabilnego identyfikatora, potem zawężam zakres do komponentu, a dopiero na końcu dokładam tekst albo relację.
- Najpierw wybieraj stabilny atrybut, najlepiej przygotowany specjalnie do testów.
- Zawężaj locatory do formularza, sekcji albo komponentu, zamiast przeszukiwać cały DOM.
- Używaj tekstu tam, gdzie naprawdę jest to trwałe i przewidywalne.
- Unikaj długich ścieżek z wieloma poziomami
div, bo to zwykle sygnał, że test opisuje layout zamiast zachowania. - Traktuj indeksy jako plan B, nie jako fundament locatora.