skip to content

Shared and Isolated Scope

Whether a called feature's variables come back as a result object or land straight in the caller. One keyword decides it, and interviewers like how quietly that flips.

on this pageshow

explore

questions

5

In a Karate feature file, what is the difference between `* call read('login.feature')` and `* def auth = call read('login.feature')`?

level: juniorimportance: must knowfreq 70%

answer

  1. Look at the shape of the line
  2. One keyword, not a flag
  3. Does the result get a name?
  4. def means isolated, bare call means shared

basics

~20 s

The def assignment decides scope. A bare call read('login.feature') merges every variable the called feature defined into the caller; def auth = call read('login.feature') isolates the call and hands those variables back as a map instead.

solid answer

~40 s

Karate has no flag for this - the **presence of a result variable** is the switch. When a `call` step has no `def`, the call runs in **shared scope**: the called feature works on the caller's variables, and everything it defines (plus any `configure` setting it changes) is left behind in the calling scenario, overwriting same-named variables. When you write `* def auth = call read('login.feature')`, the call is **isolated**: the callee still sees the caller's variables, but as copies, and its own variables come back only inside the `auth` map. So the same feature file behaves like an include directive in one form and like a function returning a value in the other. `karate.call(true, 'login.feature')` is the explicit way to ask for shared scope from JavaScript.

code

gherkin · 12 lines
gherkin
Feature: two ways to call the same file

Background:
  # shared scope: token and userId land right here
  * call read('classpath:common/login.feature') { user: 'john' }

Scenario: shared
  * print token

Scenario: isolated
  * def auth = call read('classpath:common/login.feature') { user: 'jane' }
  * print auth.token

go deeper

for a junior

Remember the shape: a call step with no def shares scope, and adding def makes it isolated. Practise both forms against one helper feature and print a variable after each.

for a middle

Be able to say what shared scope hands over - the caller's live variable map and its configuration - and what an isolated call copies instead. Name overwriting as the cost of sharing.

for a senior

Treat the def as an interface decision. Shared-scope helpers should be a short, deliberate list; anything called from many places is safer isolated so each caller names what it got.

for a principal

Set the convention across the suite: which reusable features are allowed to mutate caller scope, how they are named so the mutation is visible, and how reviewers spot a def added or removed by accident.

## One keyword flips the whole model A Karate feature file is callable, and `call read('login.feature')` runs it. What surprises people is that the very same line means two different things depending on whether its result is assigned to anything: ```gherkin # shared scope - login.feature's variables become this scenario's variables * call read('classpath:common/login.feature') * print token # isolated scope - the same feature, but its variables arrive inside auth * def auth = call read('classpath:common/login.feature') * print auth.token ``` There is no `shared: true` option, no annotation and no configuration key anywhere in this decision. The runtime simply asks whether the step carried a result variable. No result variable means shared scope. That is the whole rule, and it is why the shape of the line matters more than anything in it. ## What shared scope actually does In shared scope the called feature is handed the calling scenario's own variable map and its live configuration object rather than copies. The consequences follow directly: - Every `def` in the called feature lands in the caller and is visible to the steps after the `call`. - A variable the caller already had **is overwritten** if the callee defines the same name. - A `configure` step inside the callee - headers, proxy, SSL, timeouts - also takes effect in the caller. - Cookies collected by the callee stay in the caller's session. That is exactly the behaviour you want for a sign-in or common-setup feature: one line at the top of a `Background` sets up everything the rest of the file needs. Karate's own documentation compares it to an `include` directive, and warns in the same breath that it is easy to clobber a caller variable by accident. ## What isolated scope actually does With `def auth = ...` the callee gets a shallow copy of the caller's variables instead of the map itself. It can still *read* everything the caller had - that is deliberate, and it is how a called feature reaches values it was never passed - but its writes stay local. When the call finishes, the runtime builds a result map from the variables the called feature contributed itself and assigns it to `auth`. Configuration changes made inside the callee do **not** travel back. | | `* call read('x.feature')` | `* def out = call read('x.feature')` | |---|---|---| | Caller's variables readable by the callee | yes | yes, as copies | | Callee's new variables reach the caller | directly, by name | only inside `out` | | Callee's `configure` reaches the caller | yes | no | | Caller variable of the same name | overwritten | untouched | | Caller's `configure` reaches the callee | yes | yes | Note the last row: configuration always flows **downward** in both scopes. Only the return direction differs. A few properties hold whichever form you chose, and knowing them stops most of the guessing: - The callee always runs **every scenario** the called file contains, and tag filters that apply to a top-level run are not applied to it. - The call argument's keys are set as ordinary variables inside the callee in both scopes. - A failure inside the callee propagates to the caller and fails the calling step; it is not returned as data. - An array argument turns either form into one run per element. ## Why this rule exists at all A called feature does not run the `karate-config.js` chain - that is evaluated once per top-level scenario and skipped entirely for a callee. So a called feature starts with nothing of its own. Everything it can see comes from the caller's scope or from the call argument, which makes the question "does its work come back?" the single interesting decision about a feature call. Karate answers it with the syntax rather than a parameter. ## The traps 1. **Adding a `def` to "capture the result" silently disables sharing.** A `Background` line that used to set up ten variables now sets up one map, and the ten steps that referenced those variables by name start failing on undefined values. 2. **Removing a `def` silently enables sharing.** The callee's variables now overwrite the caller's, which can quietly change a value the rest of the scenario depends on. 3. **Shared scope is not "the callee sees my variables".** It sees them either way. Shared scope is about what comes *back*. 4. **The JavaScript form defaults the other way.** `karate.call('login.feature')` is isolated even though the `call` step of the same name is shared; you opt in with a boolean first argument, `karate.call(true, 'login.feature')`. ## Choosing between them Use shared scope for authentication and common-setup features whose entire purpose is to leave state behind, and keep them few and well-named so the overwrite risk stays visible. Use an isolated call whenever the feature is really a function - it computes something, and you want the answer under a name you chose. A helper that is called from many places is usually easier to reason about isolated, because every caller can see, in its own file, exactly which variable the call produced.

  • Can the called feature read the caller's variables when the call is isolated?
    Yes. Karate always makes the caller's variables visible to the callee; in an isolated call they are shallow copies, so the callee can read them but its writes do not reach back. Isolation controls the return direction, not the inbound one.
  • What happens to a variable the caller already has when a shared-scope call defines the same name?
    It is overwritten. Shared scope hands the callee the caller's own variable map, so a `def` in the called feature replaces the caller's value for the rest of the scenario. This is the main reason shared-scope helpers should be few and obviously named.
  • How do you get shared scope from a conditional line?
    Use the JavaScript form with a boolean first argument: `* if (env == 'dev') karate.call(true, 'login.feature')`. Plain `karate.call('login.feature')` is isolated, so the boolean is what restores the behaviour of the bare `call` step.

A bare call is an include directive - the file's contents are pasted into your scope. Adding def turns the same file into a function call whose answer you catch in a variable.

saying these in an interview costs you the question

  • Claiming a flag or configure key switches shared scope on
  • Thinking the callee cannot see caller variables unless scope is shared
  • Assuming def only names the result and changes nothing else
  • Believing a called feature re-runs the config chain for itself
  • Saying shared scope copies variables back after the call finishes
open as a page

In Karate, when you write `* def result = call read('helper.feature') { id: 5 }`, what exactly does `result` hold?

level: middleimportance: must knowfreq 52%

basics

~10 s

A map of the variables the called feature contributed itself, taken from the last scenario it executed. Caller variables it merely inherited to read are excluded, and the call argument's keys are included.

open as a page

In Karate, how does `* karate.call('login.feature')` differ from the step `* call read('login.feature')`, and how do you get shared scope from the JavaScript form?

level: middleimportance: should knowfreq 38%

basics

~10 s

They default to opposite scopes. The bare call step shares scope; karate.call(path) is isolated and simply returns the result map. Pass a boolean first argument, karate.call(true, path), to share scope from JavaScript.

open as a page

A reusable Karate feature ends with `* configure headers = read('headers.js')`. Called as `* def auth = call read('auth.feature')`, the caller's next request still goes out without those headers. Why, and what are the two ways to fix it?

level: seniorimportance: should knowfreq 42%

basics

~10 s

The def made the call isolated, and configuration travels downward into a callee but never back up. Either drop the def so the call shares scope, or repeat the configure step in the caller.

open as a page

Inside a Karate feature invoked with `* call read('helper.feature') { id: 5 }`, what do the `__arg` and `__loop` variables hold?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

__arg holds the whole call argument, here the map { id: 5 }, or null when none was passed. __loop holds the zero-based iteration index when the feature is called once per array element, and -1 otherwise.

open as a page