karate.callSingle Caching in Parallel Runs: Shared State and the JUnit 4 to JUnit 5 Boundary
0 reputation · 10 Nov 2024, 06:20 UTC
0 reputation · 10 Nov 2024, 06:20 UTC
Karate's callSingle feature is intended to execute a feature once and cache its result for the duration of a test run, so parallel scenarios can share setup data such as tokens or seed records. The cache is scoped to the Karate runtime or JVM, not to an individual feature file. The JUnit 4 to JUnit 5 runner transition in Karate 1.x is a compatibility boundary that changes how IDEs and build tools discover tests.
The unresolved design decision is whether the object returned by callSingle should be treated as immutable shared state or defensively copied per scenario. Because parallel scenarios receive the same cached reference, a mutation in one scenario can affect another, producing order-dependent behavior. Mixing JUnit 4 and JUnit 5 runner styles may also affect test discovery in some build setups.
Version-specific behavior for cache scope and report aggregation should be confirmed against the Karate version on the classpath.
Questions:
callSingle return a shared mutable reference in Karate 1.x, and is that contract documented?@Karate.Test annotation interact with parallel execution and aggregated HTML reports compared with the JUnit 4 runner?31275 reputation · 10 Nov 2024, 15:58 UTC
Yes, callSingle returns the exact same object reference to every scenario in the same JVM. The cache lives in a static ConcurrentMap keyed by feature path (see CallSingleCache in Karate source). No defensive copy is made on retrieval. If one scenario mutates the returned map or list, every other scenario sees that mutation — this is the root cause of order-dependent flakiness in parallel runs.
The Karate reference documentation states "run once, reuse result" but does not explicitly warn that the cached object is shared mutable state. Treat the returned value as effectively immutable by convention.
karate-junit4)forkCount > 1 — each fork gets its own JVM and therefore its own callSingle cache.@Karate.Test with karate-junit5)forkCount is also configured), so they share the same static cache.junit.jupiter.execution.parallel.enabled=true runs methods concurrently, increasing contention on the shared cached object.karate.junit5.report=true to let Karate's reporter aggregate into a single cucumber-html-report within that JVM. Cross-fork aggregation still needs a separate step (e.g., karate-report Maven plugin).Discouraged. Surefire runs JUnit 4 tests; Failsafe (or Surefire with junit-platform-engine) runs JUnit 5 tests. They execute in separate JVM forks, so callSingle caches are not shared across the boundary — each fork re-runs its own setup.
Return immutable data (JSON, map of primitives) or a factory function from callSingle. If you must mutate, deep-copy in the scenario:
def setup = callSingle('classpath:auth/setup.feature')
def local = copy $setup # Karate's built-in deep copy
local.newKey = 'value' # safe mutationmvn dependency:tree | grep karate (behavior matches CallSingleCache.java of that version).-Dkarate.log.level=DEBUG — observe cache hits/misses and fork boundaries in console.callSingle a feature returning a map, then each adds a distinct key. Inspect whether keys leak across scenarios.target/karate-reports after a parallel run: multiple karate-summary.json files = separate forks; single file = single-JVM parallelism.forkCount and junit.jupiter.execution.parallel.mode to understand actual parallelism.Karate.version at runtime if on an older release.One diagnostic that changes the recommendation: What is the exact Karate version on your classpath (output of mvn dependency:tree | grep karate)? Pre-1.0 cache keys behave differently.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.