In a Rego test, what does the with keyword replace and for how long?
answer
- scoped, not global
- one expression, then gone
- input cannot be assigned
- a path under data can be swapped too
- the waived branch needs no real waiver
basics
~20 swith substitutes a document only while the single expression it is attached to is evaluated. with input as {...} swaps the whole input document, and with data.waivers as {...} swaps that one path under data. Nothing persists afterwards.
solid answer
~50 s`with` is a modifier on one expression: `count(deny) == 1 with input as no_probe` evaluates that expression against the given input and nothing else in the file is affected. You need it because `input` is supplied by the caller and cannot be assigned inside a policy, so a test has no other way to feed a manifest to the rule. The same keyword replaces data documents by path — `with data.waivers as {"payments-api": {...}}` — which is how you test the waived branch without shipping a real waiver file or a bundle. Modifiers chain, so one expression can stub both input and data at once. Because the scope is a single expression, each test can pin its own world: one asserting the deny fires with no waivers, one asserting the same manifest produces no message once a waiver is present.
code
rego · 15 linespackage workloads
no_probe := {
"kind": "Deployment",
"metadata": {"name": "payments-api"},
"spec": {"template": {"spec": {"containers": [{"name": "api"}]}}},
}
test_missing_probe_is_denied if {
count(deny) == 1 with input as no_probe with data.waivers as {}
}
test_waived_workload_yields_no_message if {
count(deny) == 0 with input as no_probe with data.waivers as {"payments-api": {"rule": "require-readiness-probe"}}
}go deeper
Know that with input as feeds a fixture document to the rule for one expression, and that it is how a test drives a policy that expects a manifest.
Explain the expression-level scope, why input cannot be assigned, and how a data path is stubbed so a rule that reads a waiver document can be tested on all its branches.
Show the three-branch discipline — allowed, denied, waived — and be honest about what a stubbed waiver proves and what it leaves untested outside the suite.
Take a position on whether an exception path may ship without a covering test, and on who is accountable when a waiver mechanism works in the suite but not in the cluster.
## The shape of the modifier `with` attaches to an **expression**, not to a rule, a file or a run: ``` <expression> with <target> as <value> ``` The target is `input`, or a path under `data`. The value is any term — usually a composite literal or a fixture rule defined elsewhere in the test file. While that one expression is evaluated, the target document is replaced by the value; the moment evaluation of the expression finishes, the substitution is gone. That scoping is the whole design. There is no global fixture, no `beforeEach`, and no state carried between tests, which is why two tests in the same file can assert opposite outcomes for the same fixture without interfering. ## Why a test cannot simply assign input `input` is not a variable. It is the document the caller — the admission webhook, the CI job, the `opa eval` invocation — hands to the policy, and a policy cannot write to it. `with input as ...` is the only mechanism a policy author has to say "evaluate as if the caller had sent this". This is exactly what makes Rego tests cheap: the unit under test is the same rule the gate will evaluate, driven by a document of the same shape the gate will send. ## Replacing data, and why the waived branch needs it A realistic gate rule reads more than the object under review. A rule that denies a workload with no readiness probe will normally also consult a waiver document so an approved exception does not keep blocking: ``` deny contains msg if { input.kind == "Deployment" not has_readiness_probe(input) not data.waivers[input.metadata.name] msg := sprintf("%s: no readinessProbe on container", [input.metadata.name]) } ``` That rule now has three behaviours worth pinning: it allows a compliant workload, it denies a non-compliant one, and it stays quiet for a non-compliant workload that has a waiver. Only the first two can be driven by `input` alone. The third needs the waiver document, and `with data.waivers as {...}` supplies it for that one evaluation — no bundle to build, no fixture file on disk, and no risk that a stray waiver in the real data set makes a test pass for the wrong reason. Keep the replacement narrow. Stub `data.waivers`, the specific path the rule reads, rather than reaching for the whole `data` document; a narrow stub says in one line exactly which external input the test is controlling. ## Chaining Several modifiers can hang off the same expression and are applied together: `count(deny) == 0 with input as no_probe with data.waivers as {"payments-api": {"rule": "require-readiness-probe"}}` Read it as: evaluate `count(deny) == 0` in a world where the input is that Deployment and the waiver document contains that entry. Current OPA also lets `with` replace a rule or a built-in function with a mock value, which is occasionally useful when a rule calls something the test cannot make succeed, but for gate policies the common case is the two above. ## The three-branch discipline Once you can stub both documents, the natural unit-test shape for a gate rule is one test per branch: | Branch | Fixture | Assertion | | --- | --- | --- | | allowed | compliant manifest | the deny set is empty | | denied | non-compliant manifest, no waiver | exactly one message, with the expected text | | waived | the same non-compliant manifest, waiver stubbed | the deny set is empty again | The waived row is the one people skip, and it is the one that matters most when a developer is standing at your desk. If your rule's exception path is untested, the first time anybody exercises it is the morning it is needed, under time pressure, with a release blocked. ## A caution about what the stub proves `with data.waivers as {...}` proves your rule reads the waiver document correctly. It does not prove that anything ever puts a waiver into that document, that the entry is scoped to the right rule, or that it expires. Those live outside the unit test, and a suite that is green on a stubbed waiver while the real waiver mechanism is broken is a familiar way to be surprised in production.
- Why not have the test file just define input as a rule?`input` is not a document the policy owns; it is handed in by whoever calls the policy, and Rego gives no way to assign it. `with` is the only mechanism to substitute it, and it deliberately does so per expression so that two tests in one file can present different inputs to the same rule.
- When you stub data, should you replace the whole data document or one path?One path. `with data.waivers as {...}` states precisely which external document the test is controlling and leaves everything else the policy might read intact. A blanket replacement hides which dependency the test actually cares about and makes the failure message far harder to read when the rule later starts reading something else.
- What does a green waived-branch test still not tell you?That the real waiver ever reaches that data path. The stub proves the rule consults the document and suppresses its message when an entry is present; it says nothing about how entries get there, whether they are scoped to this rule, or whether they expire. That plumbing needs its own check outside the unit suite.
saying these in an interview costs you the question
- Thinks with input as applies to the whole test file or run
- Tries to assign input directly inside the policy
- Only ever stubs input, never a data document the rule reads
- Leaves the exception branch of the rule untested