Write Rego tests as rules whose names start with test_, supply mock input with the with keyword, and run them with opa test. Add coverage to see untested lines, and fail the build when no tests run. A policy without tests is a rule you hope works.
Why test policies
A policy that blocks public buckets is a security control. A typo that makes it always pass produces silence, which looks like success. Tests give you both directions: bad input is denied, and good input is allowed, so the policy does not become either a rubber stamp or a wall.
The basics
From the OPA documentation: tests are standard Rego rules with a name prefixed by test_, typically in a file ending in _test.rego. A package suffixed _test is recommended, though not required.
package storage_test
import data.storage
test_public_bucket_denied if {
not storage.allow with input as {"bucket": {"public": true}}
}
test_private_bucket_allowed if {
storage.allow with input as {"bucket": {"public": false}}
}
The with keyword replaces the input for that evaluation. The documentation notes it can also replace data documents and called functions with mocks.
What counts as pass or fail
A test passes if it evaluates to true. It fails if it is undefined or not true, and errors on a runtime error. Rules prefixed todo_ are skipped, which gives you a way to park an unwritten test visibly.
Be careful with negation. A test written as not storage.allow passes when allow is undefined, which includes the case where you misspelled a field name. Pair each denial test with an allow test that uses nearly the same input, so a typo cannot satisfy both.
Running them
opa test .loads a directory recursively.-vgives verbose output.--runor-rfilters tests by regular expression.--fail-on-emptymakes the command fail when no tests run, which catches misspelled test names in CI.--coveragereports lines evaluated and not evaluated. Add--format=jsonfor machine-readable output.
What to test for infrastructure policies
- A minimal valid resource that must pass.
- One failing example for every rule.
- Edge cases: missing fields, empty lists, nulls.
- Real plan fragments copied from past incidents or review comments.
- An exception case, if your policy supports waivers.
Keeping test inputs as small JSON documents makes them easy to review and reuse.
Wiring into the pipeline
- Run
opa testwith--fail-on-emptyon every pull request that touches policy files. - Publish the coverage figure, and watch for it dropping.
- Require a test with each new rule, as a review convention.
- Store policies in their own repository or directory with its own owners.
Safeguard's policies and gates offer a managed way to enforce rules at pull request time, and your own Rego tests remain useful for custom checks you maintain.
Quick check
Why add --fail-on-empty?
Without it, a misspelled test name means zero tests run and the build still passes. The flag turns that into a failure.
How do I give a test its input?
Use the with keyword, for example evaluating a rule with input as a small JSON object.
Sources
Last reviewed: 19 October 2026.