Testing Your Policies: A Practical Guide
Learn the best practices for unit and integration testing your policies with practical examples for OPA/Rego and HashiCorp Sentinel.
π Prerequisites
- Understanding of policy-as-code concepts.
- Experience with at least one policy framework (OPA or Sentinel).
- Basic knowledge of testing principles (unit vs. integration).
- Familiarity with CI/CD pipelines and JSON/YAML data structures.
What You'll Learn
π·οΈ Topics Covered
Why Test Your Policies?
Policy is code, and like any code, it must be tested. Unlike traditional application code, however, a faulty policy can have an immediate, widespread impact on your infrastructure. A poorly tested policy can block legitimate deployments, disrupt developer workflows, or worseβfail to catch critical security and compliance violations.
Reliability
Ensure policies correctly identify violations and allow valid configurations, preventing false positives and negatives.
Confidence
Deploy policy changes with confidence, knowing they behave as expected and won't cause unintended side effects.
Maintainability
Simplify policy updates and refactoring. A strong test suite acts as a safety net against regressions.
Automation
Enable fully automated policy deployment through a CI/CD pipeline that is gated by a reliable testing process.
The Policy Testing Pyramid
A healthy policy testing strategy follows the classic testing pyramid. The majority of your tests should be fast, isolated unit tests, supplemented by broader integration tests.
Unit Tests (The Foundation)
These form the base of the pyramid. They are fast, easy to write, and test a single policy rule or function in isolation using mock data. You should have many of these.
Integration Tests (The Middle Layer)
These test how multiple policies interact with each other. They use realistic data, such as a full Terraform plan JSON output, to validate a complete policy set.
End-to-End Tests (The Peak)
These are the least common. They involve running a full deployment in a test environment to validate the entire workflow, including the policy enforcement step. They are slow and expensive but provide the highest confidence.
Writing Your First Unit Tests (Hands-On Examples)
Unit tests are the most important part of your testing strategy. Let's see how to write them for the two most popular policy engines.
Example 1: Unit Testing an OPA/Rego Policy
OPA has a built-in testing framework. You write tests in .rego files, conventionally named <policy_name>_test.rego.
The Policy (policy.rego):
package example
import rego.v1
deny[msg] if {
input.kind == "Deployment"
not input.metadata.labels.owner
msg := "All deployments must have an 'owner' label."
} The Test (policy_test.rego):
package example
import rego.v1
# Test Case 1: A compliant deployment should have NO violations.
test_compliant_deployment if {
# 'with' keyword provides mock input for the test.
violations := deny with input as {"kind": "Deployment", "metadata": {"labels": {"owner": "dev-team"}}}
count(violations) == 0
}
# Test Case 2: A non-compliant deployment should have ONE violation.
test_noncompliant_deployment if {
violations := deny with input as {"kind": "Deployment", "metadata": {"labels": {}}}
count(violations) == 1
violations[_] == "All deployments must have an 'owner' label."
} Run the tests with the command: opa test .
Example 2: Unit Testing a Sentinel Policy
Sentinel's testing framework uses mock files in a test/ directory to simulate Terraform data.
The Policy (policy.sentinel):
import "tfplan/v2" as tfplan
# Find all S3 buckets
s3_buckets = tfplan.filter_attribute_contains(
tfplan.resource_changes, "type", "aws_s3_bucket",
)
# Rule: All buckets must have versioning enabled
versioning_is_enabled = rule {
all s3_buckets as _, instances {
all instances as _, r {
r.change.after.versioning[0].enabled is true
}
}
}
# Main rule for the policy
main = rule { versioning_is_enabled } The Test Cases (test/pass.json and test/fail.json):
// In test/pass.json
{
"resource_changes": [{
"type": "aws_s3_bucket", "change": {"after": {"versioning": [{"enabled": true}]}}
}]
}
// In test/fail.json
{
"resource_changes": [{
"type": "aws_s3_bucket", "change": {"after": {"versioning": [{"enabled": false}]}}
}]
} Run the tests with the command: sentinel test
Best Practices for a Robust Testing Strategy
π‘ Key Takeaways
- Test-Driven Development (TDD): Write your tests *before* writing your policy. This forces you to clearly define the desired behavior and ensures you have test coverage from the start.
- Use Realistic Data: While unit tests use mock data, your integration tests should use data that closely mirrors your production configurations, such as a sanitized `terraform show -json` output.
- Cover All Paths: For every policy, create tests for the "happy path" (compliant resources) and multiple "unhappy paths" (different ways a resource can be non-compliant).
- Automate in CI/CD: Integrate your policy tests into every pull request. A policy change should be treated with the same rigor as an application code change, and the pipeline should fail if tests do not pass.
- Keep Tests Independent: Ensure that each test case can run in any order without affecting the outcome of other tests. Avoid shared state between tests.