Safeguard
Developer Tools•

Integration tests against a real database for security rules

Why row filters, unique constraints and permissions need tests against a real database, and how throwaway containers make that practical for teams.

Safeguard Security Team
3 min read

Some security rules live in the database: row level filters, unique constraints, permissions, encryption settings. An in-memory fake will not enforce them the same way. Run those tests against the real database engine, started fresh in a disposable container for each test run.

Which rules need a real engine

Mocks and in-memory substitutes are good for speed. They are poor witnesses for behavior that belongs to the database itself.

  • A unique index that stops duplicate invitations or token reuse.
  • A foreign key that prevents orphaned records after deletion.
  • Row level security policies, where the database filters rows by tenant.
  • Collation and case sensitivity, which affect login name matching.
  • Transaction isolation, which affects race conditions on balances and quotas.

A fake that accepts everything will let a test pass while production rejects, or the reverse.

Throwaway containers

Testcontainers is a library for running tests against real services in Docker containers. Its Java guide describes starting a PostgreSQL container from an image name in a JUnit 5 before-all callback, running the tests, and stopping it in an after-all callback. The stated aim is to test against the same type of service you use in production, without mocks or in-memory services.

Similar libraries exist for other languages. The pattern is the same: start, migrate, test, discard.

A small checklist of security assertions

Write these as integration tests once, and they guard you for years.

  1. Two tenants insert the same natural key. Both succeed, and neither can read the other's row.
  2. A second signup with the same email in a different letter case is refused.
  3. A used one time token cannot be redeemed twice, even in parallel requests.
  4. Deleting a user removes or anonymizes the rows your retention policy says it should.
  5. The application database role cannot drop tables or read the audit schema.

The last item is easy to forget. Run your tests with the same low privilege role the app uses, not the owner role from the migration tool.

Keep them fast and honest

Share one container across a test class, reset data between tests, and run migrations from the same files you deploy. If your schema in tests comes from a different script than production, you are testing a different database.

Seed only with synthetic data. See the related post on test data privacy before you copy anything from production.

Where this fits

These tests sit in the middle of the pyramid. They are slower than unit tests, so write fewer of them and aim them at rules that only the database can prove.

Quick check

Can I use SQLite in tests for a Postgres app?

You can for pure logic. For anything that relies on engine behavior, such as row level security or locking, use the same engine as production.

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.