Calling and Reusing Feature Files

9 min read

In Rest Assured, reusable logic lives in Java helper methods or base classes. In Cucumber, step definitions are shared because they're in the same Java package. In Karate, reuse works differently: a feature file can be called as if it were a function. It accepts inputs, runs its scenario, and returns outputs. The calling feature receives those outputs as variables. This is Karate's most powerful feature for keeping large test suites maintainable.

call — invoking another feature file

The basic form:

* def result = call read('create-user.feature') { name: 'Alice', email: 'alice@test.com' }

The object after read(...) is the argument. Inside create-user.feature, the fields of that object (name, email) are available as top-level variables — no unpacking needed. Any variables defined inside the called feature's scenario are returned to the caller in the result object.

A reusable feature file that creates a user and returns the new ID:

# common/create-user.feature
Feature: Create User Helper
Scenario:
  Given url baseUrl
  And path 'users'
  And request { name: '#(name)', email: '#(email)', role: '#(role)' }
  When method post
  Then status 201
  * def createdId = response.id

Calling it:

* def result = call read('classpath:common/create-user.feature') { name: 'Alice', email: 'alice@test.com', role: 'admin' }
* def userId = result.createdId
And match userId == '#number'

result.createdId is the createdId that the called feature defined with def. Every def in the called feature becomes a property of the returned object.

Authentication with call — the login pattern

The most common use of call is centralising authentication:

# common/login.feature
Feature: Login Helper
Scenario:
  Given url baseUrl
  And path 'auth/login'
  And request { email: '#(email)', password: '#(password)' }
  When method post
  Then status 200
  * def authToken = response.token
# users/users.feature
Feature: User Management
 
Background:
  * def loginResult = call read('classpath:common/login.feature') { email: 'admin@test.com', password: 'AdminPass' }
  * def authToken = loginResult.authToken
  * header Authorization = 'Bearer ' + authToken
 
Scenario: Get all users
  Given url baseUrl
  And path 'users'
  When method get
  Then status 200

Every scenario in users.feature inherits the Authorization header from Background. The login logic is in one place — if the auth endpoint changes, you update login.feature once.

callonce — run once, share the result

call in Background re-runs before every scenario. For authentication, that means one login call per scenario — unnecessary if the token is valid for the whole test run. callonce runs the called feature only once per feature file and shares the result:

Background:
  * def loginResult = callonce read('classpath:common/login.feature') { email: 'admin@test.com', password: 'AdminPass' }
  * def authToken = loginResult.authToken
  * header Authorization = 'Bearer ' + authToken

With callonce, the login request fires exactly once no matter how many scenarios are in the file. Every scenario uses the same token. On a feature file with 20 scenarios, this replaces 20 login requests with 1 — a meaningful speedup.

Use call when you need a fresh result per scenario (e.g., creating a new user for each test). Use callonce when the result doesn't change between scenarios (e.g., an auth token).

call with an array — bulk operations

Passing an array instead of a single object runs the called feature once per element:

* def users = [
    { name: 'Alice', email: 'alice@test.com', role: 'admin' },
    { name: 'Bob',   email: 'bob@test.com',   role: 'tester' }
  ]
* def results = call read('classpath:common/create-user.feature') users
* match results[0].createdId == '#number'
* match results[1].createdId == '#number'

create-user.feature runs twice — once with Alice's data, once with Bob's. results is an array of return objects, one per invocation. This is how you set up bulk test data before a suite of assertion scenarios.

Organising reusable features

A clear structure keeps helpers findable:

src/test/java/
├── karate-config.js
├── common/
│   ├── login.feature           ← authentication helper
│   ├── create-user.feature     ← user setup helper
│   └── helpers.js              ← JavaScript utilities
├── users/
│   ├── UsersRunner.java
│   └── users.feature           ← calls common/login.feature
└── orders/
    ├── OrdersRunner.java
    └── orders.feature          ← calls common/login.feature and common/create-user.feature

Both users.feature and orders.feature call the same login helper. The token logic exists once. If the login endpoint changes — a new password policy, a different field name in the response — you fix it in common/login.feature and both test suites pick it up.

The call architecture

⚠️ Common mistakes

  • Calling a feature that has multiple scenarios. When you call a feature file, Karate expects exactly one Scenario in it — the first one runs, the rest are ignored. If your called feature has a Scenario Outline or multiple scenarios, only the first executes. Keep called features focused: one feature, one scenario, one job.
  • Forgetting that call does not share configure headers from the caller. Headers set with * configure headers in the main feature do not carry into the called feature. The called feature starts with a clean request context. If the called feature needs the auth token, pass it as an argument: call read('...' ) { token: authToken } and use #(token) inside.
  • Using callonce for features that need fresh data each time. If create-user.feature is called with callonce, all scenarios share the same createdId — the user is created once. If Scenario 2 deletes that user, Scenario 3 (which expects the user to exist) will fail. Use callonce only for idempotent helpers like authentication.

🎯 Practice task

Build a shared authentication helper and wire it to two feature files. 40–50 minutes.

  1. Create src/test/java/common/login.feature. It should accept email and password, POST to /auth/login (or simulate it with JSONPlaceholder's /users/login — JSONPlaceholder doesn't have real auth, so assert status 200 against any JSONPlaceholder endpoint and define a fake authToken), and return authToken via def.
  2. In users/users.feature, add a Background that calls login.feature with callonce. Set a header using the returned token. Write one scenario that confirms the header is present (check the HTML report's request detail).
  3. Create common/create-user.feature that accepts name, email, and role, POSTs to /users, and returns createdId. Call it from a scenario in users.feature using both the single-object form and the array form (with two users). Assert results[0].createdId == '#number'.
  4. Create an orders/orders.feature (even if the API doesn't have orders). Add a Background that also calls common/login.feature with callonce. Confirm the same login helper works from a different feature file in a different package.
  5. Demonstrate callonce vs call: in one feature, use call in Background for login. Run the feature with 5 scenarios and note in the HTML report how many times the login request appeared. Replace call with callonce and re-run. The login request should now appear exactly once.
  6. Stretch: add a teardown.feature that deletes a user by ID. After creating a user with create-user.feature, call teardown.feature at the end of the scenario to clean up. Confirm the DELETE returns 204 (or whatever the fake API returns).

Next chapter: advanced features — global authentication via karate-config.js, retry polling, parallel execution, and browser automation with Karate UI.

// tip to track lessons you complete and pick up where you left off across devices.