skip to content

A Karate runner class hard-codes `Runner.path("classpath:api").tags("@regression").karateEnv("dev").parallel(4)`. How do you get one CI job to run only `@smoke` against `qa` on eight threads without editing that class?

level: seniorimportance: should knowfreq 45%

answer

  1. One property, read at run time
  2. The compiled class never changes
  3. It overwrites, it does not merge
  4. Gradle needs it forwarded explicitly
  5. A malformed string is ignored, not fatal

basics

~10 s

Pass the karate.options system property, for example -Dkarate.options="--tags @smoke --env qa --threads 8". Karate reads it inside parallel() and overrides the builder's values, so the compiled class stays untouched.

solid answer

~40 s

Karate reads a `karate.options` system property **inside** the terminal `parallel(...)` call, just before the suite is built, and uses it to override what the builder holds. Running `mvn test -Dtest=ApiSuiteTest -Dkarate.options="--tags @smoke --env qa --threads 8"` therefore re-points the same compiled class at a different subset, environment and width. The string uses Karate's command-line grammar, and the important semantic is that it **replaces** rather than merges: supplied tags replace the builder's tags outright, and supplied paths replace the builder's paths. There are also narrower properties — `karate.env` and `karate.config.dir` — for the two most common single overrides. Under Gradle the test JVM is forked and does not inherit `-D` flags, so the `test` task must forward the property explicitly.

code

bash · 7 lines
bash
# same compiled runner, three different jobs
mvn test -Dtest=ApiSuiteTest
mvn test -Dtest=ApiSuiteTest -Dkarate.options="--tags @smoke --env qa --threads 8"
mvn test -Dtest=ApiSuiteTest -Dkarate.options="classpath:api/orders.feature:31"

# the narrower single-purpose properties
mvn test -Dtest=ApiSuiteTest -Dkarate.env=qa -Dkarate.config.dir=classpath:cfg

go deeper

for a junior

Recall that a Karate runner does not have to be edited to run a different subset: a karate.options system property supplies tags, environment, threads and paths from outside.

for a middle

Explain that the property is read inside the terminal call and overwrites builder fields, and that tags and paths are replaced outright rather than combined.

for a senior

Design for it: one runner with defaults plus per-job overrides, with the Gradle forwarding in place and the applied-options log line checked when a job runs the wrong set.

for a principal

Draw the line between selection and configuration — which tests run can live in a job's option string, but how they reach the system under test belongs in the config chain, or the pipeline slowly becomes the test design.

## The mechanism The builder holds whatever the Java source hard-coded. Immediately before the suite is constructed — that is, inside `parallel(n)`, not at construction time — Karate reads a small set of external inputs and **overwrites** builder fields with them. The main one is a single string: ``` -Dkarate.options="--tags @smoke --env qa --threads 8" ``` The string is parsed with Karate's own command-line grammar, so the options are the same ones the standalone runner accepts. In practice you will use: - **`--tags`** — replaces the tag expressions - **`--env`** — replaces the environment - **`--threads`** — replaces the thread count passed to `parallel(...)` - **`--configdir`** — replaces the `karate-config.js` location - **a trailing path or paths** — replaces what to run, down to `orders.feature:31` Two narrower properties cover the common single overrides without the option-string grammar: **`karate.env`** and **`karate.config.dir`**. ## Replace, never merge This is the semantic that decides whether the technique is safe, and it is the one candidates get wrong. - **Tags replace.** A builder holding `tags("@regression")` overridden with `--tags @smoke` runs `@smoke` scenarios. It does not run "`@regression` and `@smoke`", and it does not run their union. - **Paths replace.** A builder holding `path("classpath:api")` overridden with a trailing `classpath:api/orders.feature` runs only that file. The builder's path is discarded, not added to. The practical consequence is that an override is **total** for the dimension it touches. You cannot use `karate.options` to narrow within an already-narrowed runner — if the class says `@regression` and the job says `@smoke`, the `@regression` constraint is gone, and any scenario tagged `@smoke` but not `@regression` will now run. If you need "smoke within regression", the job has to say so: `--tags @regression --tags @smoke`, or a single expression that states both. ## What this buys you The point is **one compiled runner, many jobs**. A single class with sane defaults serves the nightly full run, the per-commit smoke job and a developer reproducing one scenario: ``` mvn test -Dtest=ApiSuiteTest # the defaults mvn test -Dtest=ApiSuiteTest -Dkarate.options="--tags @smoke --threads 8" mvn test -Dtest=ApiSuiteTest -Dkarate.options="--env qa" mvn test -Dtest=ApiSuiteTest -Dkarate.options="classpath:api/orders.feature:31" ``` The alternative — a runner class per job — puts selection logic in compiled code, where changing which tags a pipeline runs becomes a code review, a merge and a build. ## The Gradle trap Under Maven the Surefire JVM inherits the `-D` flags from the command line, so the property arrives with no work. **Under Gradle it does not** — the test task forks its own JVM and does not pass your `-D` through. The property is silently absent, Karate uses the builder defaults, and the job quietly runs the wrong set. The `test` task has to forward it: ```groovy test { systemProperty "karate.options", System.properties.getProperty("karate.options") systemProperty "karate.env", System.properties.getProperty("karate.env") outputs.upToDateWhen { false } } ``` The `upToDateWhen` line matters as much as the properties: without it Gradle can consider the test task up to date and skip the run entirely when only a system property changed. ## Diagnosing "it ran the wrong tests" Karate logs what it did with the property, which makes this a short investigation: 1. **Was it seen at all?** The runner logs at INFO that it is using the system property and echoes the raw string. No line means the property never reached the test JVM — start with the Gradle forwarding above, or a shell that ate the quotes. 2. **Was it parsed?** A malformed option string is logged as a warning and then **ignored**, and the run proceeds on the builder's defaults. That failure is quiet by design: a typo in a CI variable degrades to "ran the defaults", not to a crash. 3. **What actually took effect?** The applied values are logged as a summary line, which is the fastest way to confirm that a replace, not a merge, is what you got. ## Where to stop `karate.options` is a *selection* mechanism, not a configuration language. Environment-specific base URLs, credentials and timeouts belong in the `karate-config.js` chain that `--configdir` points at, not in an ever-growing option string in a job definition. A good rule: if the value is *which* tests run, it can live in `karate.options`; if it is *how* the tests talk to the system under test, it belongs in config.

  • A runner hard-codes `tags("@regression")` and the job passes `--tags @smoke`. Which scenarios run?
    Only `@smoke`. The override replaces the builder's tags rather than merging with them, so the `@regression` constraint disappears entirely — including for scenarios tagged `@smoke` but not `@regression`. To keep both constraints the job must state both, for example `--tags @regression --tags @smoke`.
  • The same command works under Maven and is ignored under Gradle. Why?
    Gradle's `test` task forks a JVM that does not inherit the command line's `-D` flags, so the property never reaches Karate and the builder's defaults are used. Forward it explicitly with `systemProperty "karate.options", System.properties.getProperty("karate.options")`, and add `outputs.upToDateWhen { false }` so Gradle does not skip the task.
  • What happens if the option string has a typo?
    The parse failure is logged as a warning and the string is ignored; the run continues on the builder's values. That is deliberate — a bad CI variable degrades to "ran the defaults" rather than failing the build — but it means a job can look healthy while testing the wrong set, so check the log line that echoes what was actually applied.

saying these in an interview costs you the question

  • Believing an override merges with the runner's own tags
  • Assuming a supplied path adds to the builder's paths
  • Expecting a malformed option string to fail the build
  • Passing -D to Gradle without forwarding it to the test JVM
  • Editing and rebuilding the runner class for each pipeline job
  • Putting base URLs and credentials into the option string