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?
answer
- One property, read at run time
- The compiled class never changes
- It overwrites, it does not merge
- Gradle needs it forwarded explicitly
- A malformed string is ignored, not fatal
basics
~10 sPass 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 sKarate 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# 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:cfggo deeper
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.
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.
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.
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