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.idCalling 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 200Every 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 ' + authTokenWith 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
calla feature file, Karate expects exactly oneScenarioin it — the first one runs, the rest are ignored. If your called feature has aScenario Outlineor multiple scenarios, only the first executes. Keep called features focused: one feature, one scenario, one job. - Forgetting that
calldoes not shareconfigure headersfrom the caller. Headers set with* configure headersin 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
calloncefor features that need fresh data each time. Ifcreate-user.featureis called withcallonce, all scenarios share the samecreatedId— the user is created once. If Scenario 2 deletes that user, Scenario 3 (which expects the user to exist) will fail. Usecallonceonly for idempotent helpers like authentication.
🎯 Practice task
Build a shared authentication helper and wire it to two feature files. 40–50 minutes.
- Create
src/test/java/common/login.feature. It should acceptemailandpassword, POST to/auth/login(or simulate it with JSONPlaceholder's/users/login— JSONPlaceholder doesn't have real auth, so assertstatus 200against any JSONPlaceholder endpoint and define a fakeauthToken), and returnauthTokenviadef. - In
users/users.feature, add a Background that callslogin.featurewithcallonce. Set a header using the returned token. Write one scenario that confirms the header is present (check the HTML report's request detail). - Create
common/create-user.featurethat acceptsname,email, androle, POSTs to/users, and returnscreatedId. Call it from a scenario inusers.featureusing both the single-object form and the array form (with two users). Assertresults[0].createdId == '#number'. - Create an
orders/orders.feature(even if the API doesn't have orders). Add a Background that also callscommon/login.featurewithcallonce. Confirm the same login helper works from a different feature file in a different package. - Demonstrate
calloncevscall: in one feature, usecallin Background for login. Run the feature with 5 scenarios and note in the HTML report how many times the login request appeared. Replacecallwithcallonceand re-run. The login request should now appear exactly once. - Stretch: add a
teardown.featurethat deletes a user by ID. After creating a user withcreate-user.feature, callteardown.featureat the end of the scenario to clean up. Confirm the DELETE returns204(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.