skip to content

In a Karate feature file, what type of value does an XPath on the left of a match produce for a leaf element, for an element with children, for an attribute, and for count()?

level: middleimportance: should knowfreq 40%

answer

  1. XML carries no types
  2. Leaf versus parent decides the shape
  3. Count the child elements
  4. One function returns a real number
  5. Empty is not missing

basics

~20 s

A leaf element yields its text as a string, so numbers arrive quoted. An element with children yields an XML chunk compared against an XML literal. An attribute yields its value as a string. XPath count() yields a number.

solid answer

~50 s

The XPath result is converted before the comparison, and the conversion is driven by the **node**, not by what the value looks like. An element with no child elements gives its text as a `String` - which is why `<age>5</age>` reads back as `'5'` and `match doc /root/age == 5` fails with `data types don't match` while `== '5'` passes. An element that has child elements gives a fresh XML document, so the right side should be an XML literal. An attribute such as `/root/@id` gives its value, again a `String`. The one place a real number appears is an XPath function: `match foo count(/records//record) == 3` compares against the number 3. XML has no null, so an empty tag reads back as `''`, and a path that selects nothing is not present rather than null.

code

gherkin · 9 lines
gherkin
* def doc = <root id="7"><name>John</name><age>30</age><meta><k>v</k></meta></root>
# leaf element -> string
* match doc /root/age == '30'
# attribute -> string
* match doc /root/@id == '7'
# element with children -> xml chunk
* match doc /root/meta == <meta><k>v</k></meta>
# xpath function -> number
* match doc count(/root/*) == 3

go deeper

for a junior

Quote your expected values. Text pulled out of XML is a string, so an age of 30 is asserted as '30' and only an XPath count gives you a real number.

for a middle

Explain the branch: child-element count decides text versus chunk, attributes follow the text branch, and count is parsed back into an integer. Name data types don't match as the failure.

for a senior

Watch for suites ported from JSON where quoting silently flipped. Decide where numeric coercion lives - in the assertion, in a converted JSON view, or not at all - and apply it consistently.

for a principal

The strictness is worth defending under pressure to add loose comparison. A test that accepts '30' for 30 will also accept a malformed field, which is the class of bug the suite exists to catch.

## Everything is a string unless the document says otherwise XML carries no type information. `<age>5</age>` and `<age>five</age>` are the same kind of node to a parser, and Karate does not guess. When an XPath on the left of a `match` selects a **leaf** - an element with no child elements - the value handed to the comparison is that element's text content, as a `String`. So: ```gherkin * def doc = <root><name>John</name><age>30</age></root> * match doc /root/name == 'John' * match doc /root/age == '30' # this one fails: data types don't match # * match doc /root/age == 30 ``` This is the single most common surprise on an XML suite, because the JSON habit is the opposite: a JSON `30` really is a number, and asserting `== '30'` there fails. Moving a suite from JSON to XML flips the quoting on every numeric assertion. ## The four shapes, and where each comes from | XPath selects | Value handed to match | Natural right side | |---|---|---| | element, no child elements | text content, a `String` | `'30'` | | element with child elements | a fresh XML document | an XML literal | | attribute node | the attribute value, a `String` | `'123'` | | an XPath function such as `count(...)` | a number | `3` | The first two are one branch, decided by counting the node's **child elements**, not its text. A `<teacher>` holding `<subject>` children is returned as a document you compare structurally: ```gherkin * def item = <item id="123" status="active">content</item> * match item /item/@id == '123' * match item /item/@status == 'active' ``` Attributes go down the leaf branch too, so `/root/@id` gives `'123'`, quoted, for exactly the same reason as `<age>`. ## count() is the exception, and it is deliberate An XPath function that returns a number rather than a node-set cannot be evaluated as a node-set at all. Karate evaluates it as a string and then, for `count`, parses that string back into an integer - so the comparison you write is against a number: ```gherkin * def foo = """ <records> <record index="1">a</record> <record index="2">b</record> </records> """ * match foo count(/records//record) == 2 ``` A function is recognised when the token before the opening parenthesis is lower-case letters or hyphens, so `count(`, `sum(` and `string-length(` all take that path. Only `count` has a guaranteed integer result across both Karate lines; for any other function, assert the shape you actually observe rather than assuming a number. ## Nulls, empties and missing paths Three different situations that people collapse into one: - **An empty element.** `<bar/>` has the text value `''`. XML has no concept of null, so `match foo/root/bar == ''` passes and `match foo/root/bar == '#present'` passes with it. - **A path that selects nothing.** The result is treated as not present: `match foo/root/nope == '#notpresent'` passes. This is a different state from empty, and the two are worth keeping straight in a review. - **A number-shaped string.** `'0'` is a non-empty string, so it is present and it is not `''`. Nothing about the value is coerced. ## The same rule, seen from the JSON side It helps to hold the contrast in mind. A JSON body carries types, so `{ "age": 30 }` gives a real number and `match response.age == '30'` fails. An XML body carries none, so `<age>30</age>` gives a string and `match doc /root/age == 30` fails. Same engine, same strictness, opposite quoting - and a suite that talks to a service offering both representations has to get this right per endpoint. When a service switches its default content type, the numeric assertions are the ones that break first. ## Working with the types instead of against them When an assertion really wants a number, put the conversion where it belongs rather than hoping the engine does it: 1. **Assert the string.** `match doc /root/age == '30'` is honest about what the document contains, and it fails loudly if the element starts carrying `30.0`. 2. **Use an XPath function.** `count(...)` gives a number without any conversion of your own. 3. **Convert the document.** `* json data = doc` produces a map where element text is still a string, but you then have JavaScript available: `* assert data.root.age * 1 == 30` states the coercion explicitly. What you should not do is reach for a loose comparison and hope. Karate's `match` refuses to compare a string to a number and says so - `data types don't match` - and that strictness is the feature. A suite that silently accepted `'30' == 30` would also accept `'30abc'` on a badly-encoded field, which is exactly the bug an API test exists to catch.

  • Why does match doc /root/age == 30 fail when the element holds 30?
    Because the left side is the string `'30'`. A leaf element's value is its text content, and XML has no type information for Karate to read. The comparison then has a string on one side and a number on the other, and `match` reports `data types don't match` rather than coercing. Assert `== '30'`, or count with an XPath function when you want a number.
  • How do you tell an empty element apart from a missing one?
    By the marker you assert against. An empty `<bar/>` has the text value `''`, so `== ''` and `== '#present'` both pass. A path that selects no node at all is not present, so `== '#notpresent'` passes and `== '#present'` fails. XML has no null, so there is no third state to test for.

saying these in an interview costs you the question

  • Expects numbers in XML to arrive as numbers
  • Says match coerces '30' and 30 to compare equal
  • Treats an empty element as null
  • Confuses an empty element with a missing path
  • Assumes every XPath function returns a number