skip to content

Proving the Rule

A policy is worth what its tests, its evaluation cost and the parts you can precompute say it is. Interviewers probe here because most candidates write a rule but cannot show it is right or fast.

on this pageshow

explore

questions

10

How does opa test decide which Rego rules are tests and whether they passed?

level: juniorimportance: must knowfreq 70%

answer

  1. no framework, only rules
  2. the name decides what runs
  3. a prefix triggers discovery
  4. undefined counts as failed
  5. non-zero exit gates CI

basics

~20 s

opa test evaluates every rule whose name begins with test_, in any package it loaded. A test passes if that rule evaluates to a defined value that is not false, and fails if it is undefined or false.

solid answer

~50 s

Rego has no separate test framework: a test is an ordinary rule with a reserved name prefix. `opa test <paths>` loads all `.rego` files under those paths, then evaluates each rule whose name starts with `test_` as its own query. If the rule body succeeds the rule is defined and true, and the test passes; if any expression in the body is undefined, or the rule evaluates to false, the test fails; an evaluation error is reported as an error. A `todo_test_` prefix marks a test as skipped. By convention the tests live in `policy_test.rego` beside `policy.rego` in the same package, so they can reference `deny` directly — but the file name is convention, the prefix is what triggers discovery. `opa test` exits non-zero if anything failed, which is what makes it usable as a CI gate. Teams whose gate is conftest run the same tests with `conftest verify`.

go deeper

for a junior

Be ready to say that a test is just a rule whose name starts with test_, that opa test runs those rules, and that the test fails when the rule is undefined or false.

for a middle

Explain the three outcomes of a Rego expression and how they map to pass, fail and error, plus why the tests usually share the package of the rule they exercise.

for a senior

Show how the suite is wired into the policy repository's own pipeline and what a red suite blocks, and explain what a failing test contributed by a blocked developer buys you.

for a principal

Own the argument that the rule repository is production code with the same review and test bar as any service, and decide who is allowed to merge a rule change without a covering test.

## Tests are ordinary rules Rego does not ship an assertion library, a test runner class, or a set-up/tear-down lifecycle. A unit test is a rule like any other, and the only thing that makes it a test is its name: if the rule is called `test_something`, `opa test` will run it. ```rego # workloads_test.rego package workloads no_probe := {"kind": "Deployment", "metadata": {"name": "payments-api"}} test_missing_probe_is_denied if { count(deny) == 1 with input as no_probe } ``` Everything in that file is normal Rego. `no_probe` is a plain rule holding a fixture document. `test_missing_probe_is_denied` is a rule whose body is a conjunction of expressions; if all of them succeed, the rule is defined with the value `true`. ## What discovery actually keys on `opa test ./policy` walks the given paths, loads every `.rego` file it finds — including files ending in `_test.rego`, which are not special to the loader — and then evaluates each rule whose name starts with `test_`, in every package it loaded. Two consequences people get wrong: - Putting a rule in a `_test.rego` file does **not** make it a test. Helper rules and fixtures in that file are just rules; only the `test_`-prefixed ones run. - Naming a rule `test_foo` in your *production* policy file does make it a test, and it will run. The file split is a human convention. The conventional layout is `workloads.rego` and `workloads_test.rego` side by side, both declaring `package workloads`. Sharing the package matters: it lets the test say `deny` rather than the fully qualified `data.workloads.deny`, and it lets the test reach helper rules that are not part of the policy's public surface. ## Pass, fail, skip Every Rego expression has one of three outcomes: it produces a value, it produces `false`, or it is **undefined** — meaning no variable binding satisfied it. A rule body is a conjunction, so one undefined expression makes the whole body undefined and the rule itself undefined. `opa test` maps that onto results: - **PASS** — the test rule is defined and its value is not `false`. - **FAIL** — the test rule is undefined, or evaluates to `false`. An undefined test is a failed test, not a skipped one; this is the single most useful thing to know, because a mistyped field path in a fixture silently makes an expression undefined and the failure looks identical to a genuine policy bug. - **ERROR** — evaluation raised an error rather than simply not matching. - **SKIP** — the rule is named with the `todo_test_` prefix instead. That is how you park a test you are not ready to fix without deleting it. `--verbose` prints the failing test's expressions and their bindings, which is normally enough to see which line went undefined. ## Why the exit code matters `opa test` exits non-zero when any test fails or errors. That is the whole integration story for CI: the policy repository's pipeline runs the suite over the rules, and a red suite blocks the merge of the rule change — the same shape of gate that the rules themselves impose on everyone else's changes. A policy repo whose tests do not run in CI is a policy repo whose rules can be broken silently, and the failure will not be noticed until the gate stops blocking something it should have blocked. ## Where this shows up from the other chair The most valuable pull request a policy repository receives often comes from a developer the gate blocked. They are not arguing in a ticket; they have added their real manifest as a fixture and a `test_` rule asserting what they believe should happen, and the suite is red. That PR is a bug report you can execute. Whether the outcome is "the rule was too broad, fix the rule" or "the rule was right, your manifest was wrong", the test they wrote is worth merging in some form, because it pins the behaviour that was in dispute so the next refactor cannot quietly undo the decision. ## Running the same tests under conftest If your gate is conftest rather than plain OPA, nothing about the tests changes: `conftest verify` loads the policy directory and runs the `test_`-prefixed rules, reporting each one. Conftest's own convention is that the gate rules are named `deny`, `violation` or `warn`, and your tests assert against those exactly as they would against any other rule.

  • Your gate is run by conftest rather than the opa binary — how do you run these tests?
    `conftest verify` loads the policy directory and runs the same `test_`-prefixed rules, reporting pass or fail per test and exiting non-zero on failure. The tests themselves are ordinary Rego and need no change. Conftest's convention is that the gate rules are named `deny`, `violation` or `warn`, so your assertions target those names.
  • A test fails and the output says the rule was undefined. What are the usual causes?
    An expression in the body found no binding: a fixture field path that does not exist, a comparison against the wrong literal, or the rule under test not firing at all. Undefined propagates through the conjunction, so the whole body dies at the first unsatisfied expression. Run with `--verbose` to see which expression stopped.
  • How do you park a test you cannot fix right now without deleting it?
    Rename it from `test_x` to `todo_test_x`. `opa test` reports it as skipped rather than running it, so the suite stays green while the case stays in the file, visible in review. It is better than commenting the body out, because the fixture and the intent remain readable.

It is closer to a spreadsheet than to JUnit: you write another cell, and the runner checks the cells whose label starts with the magic word.

saying these in an interview costs you the question

  • Thinks a file named _test.rego makes every rule in it a test
  • Believes an undefined test rule is skipped rather than failed
  • Expects an assert or expect built-in to exist in Rego
  • Cannot say how the suite blocks a merge in CI

context

open as a page

In a Rego test, what does the with keyword replace and for how long?

level: middleimportance: must knowfreq 58%

basics

~20 s

with 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.

open as a page

A Rego rule checking namespaces against thousands of quota records is slow — how do you find and fix it?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Profile with opa eval --profile: the top row is the comparison inside the scan, its redo count near the record count. Fix the data shape — key the inventory by namespace name so the lookup is one reference.

open as a page

What does opa test --coverage mark as covered in a Rego policy, and what does it not prove?

level: juniorimportance: should knowfreq 46%

basics

~20 s

opa test --coverage marks every expression the tests actually evaluated, reported per file with a percentage. That proves the policy text ran; it does not prove the rule decided correctly, and it says nothing about which inputs were tried.

open as a page

What does opa build --optimize do to a policy, and what must you give it?

level: middleimportance: should knowfreq 32%

basics

~20 s

It runs partial evaluation at build time, specialising the policy for named entrypoints against the data packaged in the bundle. You must supply at least one entrypoint; without one there is nothing for the optimiser to specialise towards.

open as a page

What does OPA's partial evaluation return when you mark part of the input unknown?

level: middleimportance: should knowfreq 48%

basics

~20 s

It returns a residual policy rather than a decision. OPA evaluates every expression it can already decide, then hands back the conditions that still depend on the unknown values, plus any support rules those conditions reference.

open as a page

How do you use OPA to list every database violating a backup rule held in an external inventory?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Compile the rule with the resource set declared unknown, then translate the residual conditions into the inventory's own query language and run them there. OPA produces the filter; the inventory produces the list. Never stream a million records through the engine.

open as a page

Why does a Rego test whose only assertion is deny with input as bad_pod always pass?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A partial set rule such as deny always produces a document, empty when nothing matched, and in Rego every value except false is truthy. So the expression succeeds whether or not the rule fired, and the test asserts nothing.

open as a page

What lets OPA index the rules of a Rego document, and which body shapes defeat it?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

OPA indexes rules that share a name using equality tests between a plain reference into input or data and a constant, so a query only evaluates the rules whose key matches. Negations, comprehensions, walks and helper calls cannot be indexed.

open as a page

When is owning a Rego-to-SQL residual translation layer worth it over hand-writing the compliance query?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

It pays when the same rules must be reported over several data stores and also enforced elsewhere, so one policy stays the single source of truth. For one stable rule over one store, a hand-written query is cheaper and easier to defend.

open as a page