skip to content

In Appium, how much should one `-ios predicate string` pin down, and why?

level: principalimportance: should knowfreq 38%

answer

  1. two failure modes, opposite directions
  2. a loose match is a false pass
  3. type is free, identifiers are cheap
  4. find, then assert separately

basics

~20 s

Pin the smallest set of attributes that makes the iOS match unambiguous — usually type plus one identifier. Every extra clause buys precision but costs a rewrite when that attribute changes, and makes a failure harder to read.

solid answer

~40 s

There are two ways to get this wrong on Apple platforms and they fail differently. Too loose — `label CONTAINS 'Book'` — matches several ice-rink session rows, and Find Element quietly returns the first, so the suite books the wrong session and the test still passes. Too tight — `type == 'XCUIElementTypeButton' AND name == 'rink.session.book' AND label CONTAINS 'public' AND value == '1'` — breaks whenever any one of four attributes changes, and reports one undifferentiated no-such-element for all four. The working rule is: `type` plus the single most stable identifying attribute, and a further clause only when a match is genuinely ambiguous. Anything you want to *assert* — the session time, the price, the enabled state — belongs in an assertion after the find, where a failure names the attribute that moved.

go deeper

for a junior

Know that a predicate can match more than one control and that the find returns the first. Check how many elements your expression matches before trusting it.

for a middle

Explain the trade: each clause narrows the match but adds an app behaviour that must stay true, and a no-such-element failure never says which clause broke.

for a senior

Show the discipline in practice — type plus one identifying clause, count the matches, scope rather than lengthen, and keep state checks in assertions where failures name themselves.

for a principal

Frame it as identifier coverage, not locator style. Set the rule for which attributes predicates may compare and treat long expressions as evidence the app-side contract is missing.

## Two ways to get it wrong A predicate is a filter, and every clause you add narrows it. That gives you a dial with a bad failure mode at each end, and on an ice-rink session app both ends are easy to reach. **Too loose** matches more than one control. `label CONTAINS 'Book'` matches the booking button on every session row on screen. Nothing errors: Find Element returns the first match the framework yields, the test taps it, and the run goes green having booked the 09:00 beginners' session instead of the 18:30 freestyle one. This is the more expensive failure because it is not a failure — it is a false pass that only surfaces when someone reads the booking data. **Too tight** matches nothing the moment any pinned attribute moves. `type == 'XCUIElementTypeButton' AND name == 'rink.session.book' AND label CONTAINS 'public' AND value == '1'` is four independent bets on app behaviour. When it breaks you get one message — no such element — with no indication which of the four clauses stopped being true, so triage begins by dismantling the expression. ## What each clause buys and costs | Clause | Buys | Costs | |---|---|---| | `type == 'XCUIElementType…'` | removes whole classes of near-matches, essentially free | almost nothing; the class rarely changes | | `name == '…'` | an exact, translation-proof match | depends on the app team setting an identifier | | `label CONTAINS[cd] '…'` | works with no app-side change | breaks on translation and on copy edits | | `value == '…'` | disambiguates identical controls by state | couples the locator to state that the test is often about | | a second `label`/`value` clause | resolves a genuinely ambiguous screen | one more thing that must stay true forever | The asymmetry in that table is the whole answer. `type` is nearly free, an identifier is cheap once it exists, and everything after that is a standing liability you are choosing to carry. ## A working rule 1. Always pin `type`. It costs nothing, removes accidental matches against static text and container elements, and never breaks on a copy change. 2. Add exactly one identifying clause — `name` where the app sets an identifier, otherwise the most stable fragment of `label` with `[cd]`. 3. Stop. Run Find Elements and count the matches. One match means you are done. 4. Only if the count is above one, add a third clause — and prefer scoping the find to a parent element over adding a clause, because scope is easier to read than a long conjunction. 5. Never add a clause "for safety". Every clause is a future failure with no name attached to it. ## Logic that does not belong in the predicate The strongest version of this answer separates *finding* from *checking*: - **Assertions.** If the test cares that the 18:30 session is priced at £8 and is enabled, find the row by identifier and then assert those attributes. A failed assertion names the attribute; a failed predicate does not. - **Hierarchy.** A predicate has no path steps. When the real constraint is "the button inside that cell", scope the find to the cell you already hold, or use Appium's XCUITest `-ios class chain` strategy, which is the strategy with steps. - **Ordering.** There is no index in the predicate language. If a case depends on "the third session row", the honest expression of that is Find Elements plus indexing in the test, where it is visible. ## Making the failure legible A predicate is a single string, which is why an over-stuffed one is opaque. Two habits recover the readability without loosening the match: build the predicate from named fragments in the page object so a diff shows which clause changed, and keep the naming of ice-rink identifiers systematic — `rink.session.row.<time>.book` — so a `BEGINSWITH` clause can address a family and an `==` clause can address one member, without a compound expression in either case. ## The policy a team actually needs At suite scale this stops being a locator question. The team decides which attributes iOS predicates may compare, and that decision is a contract with the app developers: identifiers on every interactive control means predicates stay short; no identifiers means every predicate compares translated copy and the suite carries the maintenance forever. Write the rule down, and treat a four-clause predicate in review as a signal that the identifier coverage is missing — not as a locator to be tuned.

  • Why is an over-loose predicate more dangerous than an over-tight one?
    An over-tight predicate fails loudly and gets fixed within the hour. An over-loose one matches several controls, Find Element returns the first, and the run passes having exercised the wrong ice-rink session row. The suite reports success while covering nothing you intended, which is the failure mode that survives for months.
  • How would you enforce a predicate-length rule across a large iOS suite?
    Not by counting clauses in review. Keep locators in page objects so they are reviewable in one place, treat any predicate with more than two clauses as a request for an app-side identifier, and track the controls that still lack one. The rule is a signal for missing identifier coverage rather than a style limit.

saying these in an interview costs you the question

  • Adds clauses for safety without checking the match count.
  • Thinks a longer predicate is inherently more reliable.
  • Puts assertions about state inside the locator expression.
  • Depends on Find Element returning the row they expect.
  • Tries to express parent or child constraints inside the predicate.