In a Karate feature file, `* def foo = [1, 2, 3, 5]`. Does `match foo !contains [5, 6]` pass, and what exactly does `!contains` negate?
answer
- the negation wraps the whole comparison
- contains itself is an all check
- not all present is not none present
- one forbidden value per step
- absent and different look identical
basics
~10 sIt passes, which surprises most people. !contains negates the whole contains result, and contains requires every expected item. Since 6 is missing, contains fails and the negation succeeds even though 5 is present.
solid answer
~40 s`!contains` is not a per-item absence check — it runs the ordinary `contains` comparison and inverts the single boolean it returns. `contains [5, 6]` demands that **both** items be present; `6` is not, so it fails, so `!contains [5, 6]` passes even though `5` is in the array. The safe reading is *'it is not the case that all of these are present'*, not *'none of these is present'*. For a one-item expected value the ambiguity disappears, which is why `match foo !contains 4` is the form worth writing. The same inversion explains the object case: `match response !contains { status: 'CLOSED' }` passes both when `status` is absent and when it holds any other value, so it cannot distinguish the two.
code
gherkin · 16 lines* def foo = [1, 2, 3, 5]
# passes - but only because 6 is missing, not because 5 is
* match foo !contains [5, 6]
# the unambiguous way to say it
* match foo !contains 6
* def obj = { bar: 1, baz: 'hello' }
# both pass: one key exists with another value, one key does not exist
* match obj !contains { bar: 2 }
* match obj !contains { huh: '#notnull' }
# to assert a key is really absent, say so directly
* match obj == { bar: 1, baz: 'hello', huh: '#notpresent' }go deeper
Remember that ! is glued to contains with no space, and that the safe habit is one forbidden value per step rather than a list.
Explain the mechanism: the negation wraps the whole contains result, and since contains is an all-must-be-present check, its negation is only a not-all claim.
Treat every multi-item !contains in review as suspect, and reach for #notpresent whenever the real intent is absence rather than difference. A weak negative assertion never announces itself.
Negative assertions are where a suite's confidence quietly decays. Decide how a team expresses forbidden state, and make sure that convention is one that fails loudly when it is wrong.
## `!contains` is one negation, not many `!contains` is written as a single token — the `!` is glued to `contains` with no space — and it is implemented as a wrapper: run the plain `contains` comparison, then invert the result. Everything surprising about it follows from that one sentence, because `contains` itself is an **all** check. For the example: 1. `contains [5, 6]` searches the actual array for `5` — found at the last index. 2. It searches for `6` — not found, so `contains` fails. 3. `!contains` inverts that failure into a pass. So `match foo !contains [5, 6]` passes on `[1, 2, 3, 5]`. The step did not assert that neither `5` nor `6` is present; it asserted that they are not **both** present. Read aloud, the correct gloss is *"it is not the case that all of these are present"*. ## The safe form The ambiguity exists only for a multi-item expected value, so: - **Write one item per step.** `match foo !contains 4` is unambiguous — a single-element expected value makes "not all present" and "none present" the same statement. - For several forbidden values, write several steps rather than one list. - `match foo !contains [5, 6]` is not wrong; it just means less than it looks like it means. ```gherkin * def foo = [1, 2, 3, 5] # unambiguous, one forbidden value per step * match foo !contains 4 * match foo !contains 6 # passes, because 6 is missing - NOT because 5 is missing * match foo !contains [5, 6] ``` ## The object case: absent and different look the same The same inversion produces a second blind spot for maps: ```gherkin * def foo = { bar: 1, baz: 'hello' } * match foo !contains { bar: 2 } * match foo !contains { huh: '#notnull' } ``` Both steps pass, but for different underlying reasons: the first because `bar` exists with a different value, the second because `huh` does not exist at all. `contains` fails in both cases and the negation cannot tell you which. If you specifically need *"this key must not exist"*, do not use `!contains` — use an equality match with a `#notpresent` marker at that path, which states absence directly and fails if the key appears holding anything. One further edge worth knowing, and it is the one place the two Karate lines disagree. On Karate 1.x **`match someMap !contains {}` always passes**: the negation operator hard-codes an early return for an empty expected map — the source comment calls it a hack — whatever the actual holds. `contains {}` always passes on both lines. 2.x dropped the special case, so `!contains {}` still passes against a non-empty map but **fails** when the actual map is empty as well: the comparison then succeeds and the negation inverts it. Either way, an expected object that collapses to `{}` at runtime has stopped asserting what its author meant. ## Arrays and objects fail the same test differently It is worth being precise about which comparison is being inverted, because the two actual types reach failure by different routes: | actual | `contains` fails when | so `!contains` passes when | |---|---|---| | array | any one expected item is missing | not every expected item is present | | object | any expected key is missing, or its value does not match | not every expected pair matches | In both rows the negation is a claim about the **conjunction**, never about the individual items, and neither row tells you which item was responsible. That is the whole of it. ## What does not exist - **There is no `!contains deep`.** The negated deep operator was never implemented — the operator set has no entry for it. To assert that something nested is absent, target the nested path directly or use `#notpresent` in an equality match. - **There is no `!contains only` and no `!contains any` either.** Plain `contains` is the only member of the family with a negated spelling. On Karate 1.x the step parser reads the `only` and `any` modifiers before it ever looks at the `!`, so `match foo !contains only [...]` parses and runs as `contains only` with the negation silently dropped — the opposite assertion, green with no warning. On 2.x the token is not a recognised operator and the step is rejected outright. - **`each !contains` does exist**, and applies the negation to every element of an array: `match each response !contains { error: '#notnull' }`. - The inline form is `'#(!^part)'`, which is the embedded-expression spelling of `!contains` and composes the same way as the positive prefixes. ## Reviewing a `!contains` step Three questions catch nearly every misuse: 1. **How many items are on the right?** More than one, and the step means "not all", which is almost never what the author meant. 2. **Is the intent "absent" or "different"?** If it is absent, `#notpresent` says so and `!contains` does not. 3. **Can the expected value be empty at runtime?** If it is built from a variable or read from a file, `{}` is reachable, and the step stops asserting what you meant — silently green on 1.x, and on 2.x green or red depending on whether the actual happens to be empty too. A negative assertion is the one kind that fails silently in the wrong direction: when it is too weak it simply keeps passing, and nothing in the report says so.
- How do you assert that a key is genuinely absent from a JSON object?Use an equality match with the `#notpresent` marker at that path, for example `match foo == { a: '#number', b: '#notpresent' }`. It states absence directly and fails if the key turns up holding anything. `!contains` cannot express it, because it passes equally when the key exists holding a different value.
- Is there a `!contains deep`?No. The negated deep operator does not exist in the operator set, and the documentation says as much. If a nested chunk must be absent, assert on that nested path directly, or use `#notpresent` inside an equality match at the level where the key would appear.
- Does `each !contains` work?Yes. `match each response !contains { error: '#notnull' }` applies the negated containment to every element of the array. The same multi-item caveat applies within each element, so keep the expected value down to one key or one item per step to keep the meaning unambiguous.
saying these in an interview costs you the question
- Reads !contains [a, b] as neither a nor b present
- Uses !contains to assert that a key is absent
- Thinks !contains deep exists as an operator
- Writes ! contains with a space between the tokens
- Assumes match foo !contains {} fails on a non-empty object
- Never questions a negative assertion because it stays green