Testing
The assertion
@Test
@WithTenant("acme")
void oneTenantCannotSeeAnother() {
assertThat(orders.findAll()).isNotEmpty();
assertTenantCannotSee("globex");
}assertTenantCannotSee is deliberately two-sided. Proving "tenant A sees no rows belonging
to B" is worthless alone — an empty table passes it. So it first checks, on a privileged
connection that bypasses the policy, that B's rows genuinely exist, and fails with a message
telling you to seed them if they do not. It also refuses to run with no tenant bound, and
refuses when the acting tenant is the tenant you asked about.
The fixture
static final TenantPostgres POSTGRES = TenantPostgres.start()
.withTenantTable("orders", "item varchar(255) not null")
.withRegistry("acme", "globex");
@BeforeEach
void bind() {
POSTGRES.seedRow("orders", "acme", Map.of("item", "laptop"));
POSTGRES.seedRow("orders", "globex", Map.of("item", "monitor"));
IsolationAssertions.bind(POSTGRES.applicationDataSource(), POSTGRES.privilegedDataSource());
IsolationAssertions.bindTable("orders", "tenant_id");
}withTenantTable creates the table with the index, FORCE ROW LEVEL SECURITY and a
correctly guarded policy. withUnprotectedTable creates one without a policy, for testing
the discriminator strategy on its own.
Two DataSources, and they are not interchangeable
applicationDataSource()— a least-privileged role, neither superuser nor owner. This is what the code under test uses, and it is subject to the policy.privilegedDataSource()— bypasses the policy. For seeding and for proving another tenant's rows exist. Never the subject of an assertion.
Standing up a Postgres container is one line. Standing up one that can prove isolation is not, and the trap is the connection you test through: Testcontainers hands you a superuser, superusers bypass RLS outright, and a suite written against that connection passes whether or not the policy works — including after someone deletes it.
The pool defaults to one connection, so a tenant left behind on a recycled connection is observed deterministically rather than occasionally.
Break it and watch it fail
Green tests are not evidence. After a test passes, break the implementation and confirm the
test goes red. Every isolation claim in this library was checked that way, and it caught
something real every time — including a BUILD SUCCESS with zero tests executed, an
@Async test that would have passed if the work never left the calling thread, and an
assertTenantCannotSee that passed with the library switched off entirely.
The cheapest version of this for your own code:
mvn test -Dspring.autoconfigure.exclude=io.tenantlayer.autoconfigure.TenantLayerAutoConfiguration
If your isolation tests still pass with TenantLayer turned off, they are not testing isolation.