How does opa test decide which Rego rules are tests and whether they passed?
answer
- no framework, only rules
- the name decides what runs
- a prefix triggers discovery
- undefined counts as failed
- non-zero exit gates CI
basics
~20 sopa 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 sRego 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
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.
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.
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.
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