skip to content

In a Karate feature file, why can the XPath written into a match step not contain a variable, and what does karate.xmlPath(xml, expression) give you instead?

level: middleimportance: should knowfreq 36%

answer

  1. The step text is not a template
  2. A path in a step is literal
  3. Move the path into JavaScript
  4. A function on the karate object
  5. Assign first, then assert

basics

~20 s

A match step's path is read literally from the step text, so nothing is interpolated. karate.xmlPath takes the path as an ordinary JavaScript string, so you can build it by concatenation, assign the result, and assert on that.

solid answer

~50 s

Karate splits a `match` left side into a variable part and a path part by reading the step text, so the path is a **literal**: `match xml /query/elementName/foo` looks for a child actually called `elementName`. The upstream reference says the named-path form cannot be dynamic and tells you to use an extra step. That step is `karate.xmlPath(xml, expression)` - a function on the built-in `karate` object whose second argument is a normal string, so `karate.xmlPath(xml, '/query/' + elementName + '/foo')` works, and so does any path assembled in a JavaScript function or inside `karate.forEach`. Assign the result with `def` and assert on the variable. It accepts either a parsed XML node or a raw XML string as its first argument, and it returns the same converted value the match left side would have produced, including a number for `count(...)`.

code

gherkin · 9 lines
gherkin
* def xml = <query><name><foo>bar</foo></name></query>
* def elementName = 'name'
* def name = karate.xmlPath(xml, '/query/' + elementName + '/foo')
* match name == 'bar'
* def queryName = karate.xmlPath(xml, '/query/' + elementName)
* match queryName == <name><foo>bar</foo></name>
* def foo = <root><a>1</a><a>2</a></root>
* def tmp = karate.xmlPath(foo, 'count(/root/a)')
* match tmp == 2

go deeper

for a junior

Recall the rule of thumb: a fixed path goes in the step, a computed path goes through karate.xmlPath. Assign the result with def and assert on that variable.

for a middle

Explain that the step's left side is split textually, so nothing interpolates, and that the helper takes the path as a plain JavaScript string with the same conversion rules on the way back.

for a senior

Watch for suites that push every path through the helper because one of them had to be dynamic. Fixed paths belong in the step, where they read better and show up in the report.

for a principal

Set the convention early. A codebase that mixes literal paths and computed paths without a rule pays for it in review, because the reader cannot tell at a glance which steps are data-dependent.

## Why the step's path is literal There is no glue layer here. Karate reads a `match` step, dispatches on its leading keyword, and then parses what remains into a **variable part** and a **path part** using the text as written. Nothing runs a template engine over it first. That is deliberate - it is what makes `match response /Envelope/Body/Result == 'OK'` unambiguous - but it means a path cannot be assembled from data: ```gherkin * def xml = <query><name><foo>bar</foo></name></query> * def elementName = 'name' # this looks for a child literally called elementName, and finds nothing * match xml /query/elementName/foo == 'bar' ``` The reference documentation states the restriction for the named-path form of the left side directly, and points at an extra step as the fix. ## The escape hatch `karate.xmlPath(xml, expression)` is a function on the built-in `karate` object. Its second argument is an ordinary string, evaluated by the JavaScript engine like any other expression, so it can be concatenated, interpolated, or returned from a function: ```gherkin * def xml = <query><name><foo>bar</foo></name></query> * def elementName = 'name' * def name = karate.xmlPath(xml, '/query/' + elementName + '/foo') * match name == 'bar' * def queryName = karate.xmlPath(xml, '/query/' + elementName) * match queryName == <name><foo>bar</foo></name> ``` The pattern is always the same: compute, `def`, then `match` on the plain variable. The assertion stays readable because the interesting part - the path - has a name. ## What it returns It applies exactly the same conversion the match left side applies, so nothing new has to be learned: - an element with no child elements gives its **text**, as a string; - an element with children gives an **XML chunk**; - two or more nodes give a **list**; - `karate.xmlPath(foo, 'count(/root/a)')` gives a **number**, so `match tmp == 2` is the right shape. The first argument is flexible: a variable already parsed as XML, or a raw XML string, which the function parses for you. ## Where it earns its place Three situations, in rough order of how often they come up: 1. **A path element that comes from data.** A table-driven scenario walking a list of field names, or a path whose segment is an id returned by an earlier call. 2. **Inside a JavaScript function.** Feature steps are not available in a JS body, so when a helper function has to read a document, `karate.xmlPath` is the only way in. The same is true inside a `karate.forEach` callback. 3. **A path built once and reused.** Assign the path to a variable, then reuse it across several documents, keeping the XPath in one place instead of copied across four steps. ## The habit to avoid Do not reach for string surgery in the step text itself - embedded expressions belong in the payload you build, not in the path you read. And do not skip the `def`: writing the call inline as the left side of a match works, but it hides the value from `print`, and the first thing you want when an XML assertion fails is to see what the path actually selected. ```gherkin * def path = '/records/record[' + index + ']' * def value = karate.xmlPath(response, path) * print 'selected:', value * match value == expected ``` ## What it does not change Using the helper does not soften any of the other rules. A path that selects nothing still selects nothing; a single node is still unwrapped rather than wrapped in a list; a leaf element's value is still a string, so a computed path over `<age>30</age>` gives `'30'` exactly as the step form does. Nothing about moving the path into JavaScript makes the result more JavaScript-ish - the conversion happens before the value reaches the engine, so both routes hand you the same object. Nor does it change how the assertion is reported. The report shows the step that failed, so a `match value == expected` line tells a reader less about the intent than `match response /Envelope/Body/Result == 'OK'` does. That is a genuine cost of the indirection, and the reason to reach for it only when the path really is dynamic. ## The JSON twin, and the reason both exist The same shape exists for JSON, and for the same reason: a JsonPath written into a step is also literal. Recognising that the pair are siblings is the point - the rule is not "XPath is special", it is "a path in a step is text, a path in a function is data". Once that lands, the choice between the two forms stops being a matter of taste: write the path in the step whenever it is fixed, because it reads better and appears in the report; move it into the function the moment any part of it depends on a value.

  • Does karate.xmlPath need its first argument to be already parsed as XML?
    No. It accepts a variable that already holds a parsed XML node, and it also accepts a raw XML string, which it parses for you. So `karate.xmlPath('<root><foo>bar</foo></root>', '/root/foo')` works as written. That matters inside a JavaScript helper that received a payload as text and has no feature step available to convert it first.
  • Why not just write the call as the left side of the match?
    It does work - the left side accepts a function call. But assigning first with `def` gives the value a name you can `print` when the assertion fails, and it keeps the failing step short enough to read in the report. When an XML assertion fails, the first question is always what the path actually selected, and a named variable answers it in one line.

saying these in an interview costs you the question

  • Thinks the step's XPath is interpolated like a template
  • Tries to embed #(name) inside the path of a match step
  • Believes feature steps are callable from a JS function
  • Says karate.xmlPath only accepts a parsed node
  • Expects a different return shape than the step form