Safeguard
Developer Tools•

How to write unit tests for Rego policies with opa test

Policy as code needs tests. How to write Rego test rules, mock input with the with keyword, check coverage and avoid silently empty test runs in CI.

Safeguard Security Team
Updated 3 min read

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.
  • -v gives verbose output.
  • --run or -r filters tests by regular expression.
  • --fail-on-empty makes the command fail when no tests run, which catches misspelled test names in CI.
  • --coverage reports lines evaluated and not evaluated. Add --format=json for machine-readable output.

What to test for infrastructure policies

  1. A minimal valid resource that must pass.
  2. One failing example for every rule.
  3. Edge cases: missing fields, empty lists, nulls.
  4. Real plan fragments copied from past incidents or review comments.
  5. 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

  1. Run opa test with --fail-on-empty on every pull request that touches policy files.
  2. Publish the coverage figure, and watch for it dropping.
  3. Require a test with each new rule, as a review convention.
  4. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.