To test Firestore security rules, use the @firebase/rules-unit-testing library alongside the Firebase Emulator Suite. Create authenticated and unauthenticated test contexts with initializeTestEnvironment(), then verify rule behavior with assertSucceeds() for operations that should be allowed and assertFails() for operations that should be denied. Write tests for every CRUD operation, every user role, and every edge case to catch permission gaps before deploying to production.
Testing Firestore Security Rules with Automated Tests
Firestore security rules are your last line of defense against unauthorized data access. Deploying untested rules risks exposing user data or breaking your app. This tutorial covers writing a comprehensive automated test suite that verifies your rules allow the right access patterns and deny everything else, using the @firebase/rules-unit-testing library and the Firebase Emulator Suite.
Prerequisites
- Firebase CLI installed and a project initialized with firebase init
- Firestore emulator configured (firebase init emulators, select Firestore)
- A firestore.rules file with rules to test
- Node.js 18+ and a test runner (vitest, jest, or mocha)
Step-by-step guide
Install the testing library and set up your test file
Install the testing library and set up your test file
Install @firebase/rules-unit-testing as a dev dependency. This package provides the initializeTestEnvironment function that creates a test harness connected to your local Firestore emulator. Import the assertion helpers assertSucceeds and assertFails along with Firestore operations from the firebase/firestore package.
1# Install dependencies2npm install --save-dev @firebase/rules-unit-testing vitest34# Create test file5# tests/firestore.rules.test.tsExpected result: The testing library is installed and ready for your test file.
Initialize the test environment with your rules file
Initialize the test environment with your rules file
In your test setup, call initializeTestEnvironment() with your project ID and the path to your firestore.rules file. The function reads your rules and loads them into the Firestore emulator. Use beforeAll to set up the environment once, afterEach to clear data between tests, and afterAll to clean up resources.
1import {2 initializeTestEnvironment,3 assertSucceeds,4 assertFails,5 RulesTestEnvironment6} from '@firebase/rules-unit-testing';7import { doc, getDoc, setDoc, updateDoc, deleteDoc, collection, getDocs } from 'firebase/firestore';8import { readFileSync } from 'fs';910let testEnv: RulesTestEnvironment;1112beforeAll(async () => {13 testEnv = await initializeTestEnvironment({14 projectId: 'rules-test-project',15 firestore: {16 rules: readFileSync('firestore.rules', 'utf8'),17 host: '127.0.0.1',18 port: 808019 }20 });21});2223afterEach(async () => {24 await testEnv.clearFirestore();25});2627afterAll(async () => {28 await testEnv.cleanup();29});Expected result: The test environment is initialized with your rules loaded into the local Firestore emulator.
Test authenticated read and write operations
Test authenticated read and write operations
Use testEnv.authenticatedContext('uid') to create a Firestore client that acts as an authenticated user with the specified UID. Write tests that verify authenticated users can perform the operations your rules allow. For each test, create the context, get the Firestore instance, and wrap the operation in assertSucceeds() or assertFails().
1describe('User profiles', () => {2 test('authenticated user can read their own profile', async () => {3 const alice = testEnv.authenticatedContext('alice');4 const db = alice.firestore();5 await assertSucceeds(getDoc(doc(db, 'users', 'alice')));6 });78 test('authenticated user cannot read another user profile', async () => {9 const alice = testEnv.authenticatedContext('alice');10 const db = alice.firestore();11 await assertFails(getDoc(doc(db, 'users', 'bob')));12 });1314 test('authenticated user can create their own profile', async () => {15 const alice = testEnv.authenticatedContext('alice');16 const db = alice.firestore();17 await assertSucceeds(18 setDoc(doc(db, 'users', 'alice'), {19 email: 'alice@test.com',20 displayName: 'Alice',21 role: 'member'22 })23 );24 });2526 test('user cannot create profile for another user', async () => {27 const alice = testEnv.authenticatedContext('alice');28 const db = alice.firestore();29 await assertFails(30 setDoc(doc(db, 'users', 'bob'), {31 email: 'bob@test.com',32 displayName: 'Bob',33 role: 'member'34 })35 );36 });37});Expected result: Tests confirm that users can only read and write their own profile documents.
Test unauthenticated access and data validation
Test unauthenticated access and data validation
Use testEnv.unauthenticatedContext() to simulate requests without any authentication. These tests verify that your rules properly deny access to unauthenticated users. Also test data validation rules by passing invalid data and verifying the write is denied.
1describe('Unauthenticated access', () => {2 test('unauthenticated user cannot read any profile', async () => {3 const unauth = testEnv.unauthenticatedContext();4 const db = unauth.firestore();5 await assertFails(getDoc(doc(db, 'users', 'alice')));6 });78 test('unauthenticated user cannot create a post', async () => {9 const unauth = testEnv.unauthenticatedContext();10 const db = unauth.firestore();11 await assertFails(12 setDoc(doc(db, 'posts', 'post-1'), { title: 'Spam' })13 );14 });15});1617describe('Data validation', () => {18 test('user cannot set role to admin', async () => {19 const alice = testEnv.authenticatedContext('alice');20 const db = alice.firestore();21 await assertFails(22 setDoc(doc(db, 'users', 'alice'), {23 email: 'alice@test.com',24 displayName: 'Alice',25 role: 'admin' // Should be denied26 })27 );28 });2930 test('post title must be a non-empty string', async () => {31 const alice = testEnv.authenticatedContext('alice');32 const db = alice.firestore();33 await assertFails(34 setDoc(doc(db, 'posts', 'post-1'), {35 title: '', // Empty string should be denied36 authorId: 'alice'37 })38 );39 });40});Expected result: Tests confirm that unauthenticated access is denied and data validation rules reject invalid inputs.
Seed test data for rules that check existing documents
Seed test data for rules that check existing documents
Some rules check existing document data (resource.data) to decide access — for example, only the post author can delete it. To test these rules, you need to seed data before the test. Use testEnv.withSecurityRulesDisabled() to get a Firestore client that bypasses all rules, write the seed data, then run your actual test with rules enabled.
1describe('Owner-based access', () => {2 test('post author can delete their own post', async () => {3 // Seed a post owned by alice4 await testEnv.withSecurityRulesDisabled(async (ctx) => {5 const db = ctx.firestore();6 await setDoc(doc(db, 'posts', 'post-1'), {7 title: 'Alice Post',8 authorId: 'alice',9 content: 'Hello'10 });11 });1213 // Now test as alice — should be allowed to delete14 const alice = testEnv.authenticatedContext('alice');15 await assertSucceeds(deleteDoc(doc(alice.firestore(), 'posts', 'post-1')));16 });1718 test('non-author cannot delete the post', async () => {19 // Seed a post owned by alice20 await testEnv.withSecurityRulesDisabled(async (ctx) => {21 const db = ctx.firestore();22 await setDoc(doc(db, 'posts', 'post-1'), {23 title: 'Alice Post',24 authorId: 'alice',25 content: 'Hello'26 });27 });2829 // Test as bob — should be denied30 const bob = testEnv.authenticatedContext('bob');31 await assertFails(deleteDoc(doc(bob.firestore(), 'posts', 'post-1')));32 });33});Expected result: Tests verify that only the document owner can delete it, using seeded data to check existing document rules.
Test custom claims and role-based rules
Test custom claims and role-based rules
If your rules use custom claims (request.auth.token.role), pass claims as the second argument to authenticatedContext(). This simulates a user whose ID token contains the specified custom claims, allowing you to test admin access, editor permissions, and other role-based patterns.
1describe('Admin access', () => {2 test('admin can read any user profile', async () => {3 const admin = testEnv.authenticatedContext('admin-1', { role: 'admin' });4 const db = admin.firestore();5 await assertSucceeds(getDoc(doc(db, 'users', 'alice')));6 await assertSucceeds(getDoc(doc(db, 'users', 'bob')));7 });89 test('admin can delete any post', async () => {10 await testEnv.withSecurityRulesDisabled(async (ctx) => {11 await setDoc(doc(ctx.firestore(), 'posts', 'post-1'), {12 title: 'Test',13 authorId: 'alice'14 });15 });1617 const admin = testEnv.authenticatedContext('admin-1', { role: 'admin' });18 await assertSucceeds(deleteDoc(doc(admin.firestore(), 'posts', 'post-1')));19 });2021 test('regular user cannot access admin endpoints', async () => {22 const user = testEnv.authenticatedContext('user-1', { role: 'member' });23 const db = user.firestore();24 await assertFails(getDocs(collection(db, 'admin-logs')));25 });26});Expected result: Tests confirm that admin users have elevated access while regular users are restricted.
Complete working example
1// Complete Firestore security rules test suite2// Covers user profiles, posts, admin access, and data validation34import {5 initializeTestEnvironment,6 assertSucceeds,7 assertFails,8 RulesTestEnvironment9} from '@firebase/rules-unit-testing';10import {11 doc, getDoc, setDoc, updateDoc, deleteDoc,12 collection, getDocs13} from 'firebase/firestore';14import { readFileSync } from 'fs';1516let testEnv: RulesTestEnvironment;1718beforeAll(async () => {19 testEnv = await initializeTestEnvironment({20 projectId: 'rules-test',21 firestore: {22 rules: readFileSync('firestore.rules', 'utf8'),23 host: '127.0.0.1',24 port: 808025 }26 });27});2829afterEach(async () => await testEnv.clearFirestore());30afterAll(async () => await testEnv.cleanup());3132// Helper to seed data bypassing rules33async function seedDoc(path: string, data: Record<string, unknown>) {34 await testEnv.withSecurityRulesDisabled(async (ctx) => {35 await setDoc(doc(ctx.firestore(), path), data);36 });37}3839describe('User profiles', () => {40 test('user reads own profile', async () => {41 const ctx = testEnv.authenticatedContext('u1');42 await assertSucceeds(getDoc(doc(ctx.firestore(), 'users', 'u1')));43 });4445 test('user cannot read other profile', async () => {46 const ctx = testEnv.authenticatedContext('u1');47 await assertFails(getDoc(doc(ctx.firestore(), 'users', 'u2')));48 });4950 test('user creates own profile', async () => {51 const ctx = testEnv.authenticatedContext('u1');52 await assertSucceeds(setDoc(doc(ctx.firestore(), 'users', 'u1'), {53 email: 'u1@test.com', displayName: 'User 1', role: 'member'54 }));55 });5657 test('user cannot escalate role', async () => {58 const ctx = testEnv.authenticatedContext('u1');59 await assertFails(setDoc(doc(ctx.firestore(), 'users', 'u1'), {60 email: 'u1@test.com', displayName: 'User 1', role: 'admin'61 }));62 });63});6465describe('Posts', () => {66 test('author can delete own post', async () => {67 await seedDoc('posts/p1', { title: 'Test', authorId: 'u1' });68 const ctx = testEnv.authenticatedContext('u1');69 await assertSucceeds(deleteDoc(doc(ctx.firestore(), 'posts', 'p1')));70 });7172 test('non-author cannot delete post', async () => {73 await seedDoc('posts/p1', { title: 'Test', authorId: 'u1' });74 const ctx = testEnv.authenticatedContext('u2');75 await assertFails(deleteDoc(doc(ctx.firestore(), 'posts', 'p1')));76 });77});7879describe('Admin', () => {80 test('admin reads any profile', async () => {81 const ctx = testEnv.authenticatedContext('a1', { role: 'admin' });82 await assertSucceeds(getDoc(doc(ctx.firestore(), 'users', 'u1')));83 });84});8586describe('Unauthenticated', () => {87 test('no access to profiles', async () => {88 const ctx = testEnv.unauthenticatedContext();89 await assertFails(getDoc(doc(ctx.firestore(), 'users', 'u1')));90 });91});Common mistakes when testing Firestore Security Rules
Why it's a problem: Only testing positive cases (assertSucceeds) without testing that unauthorized access is properly denied
How to avoid: Write an assertFails test for every assertSucceeds test. If your rules allow authenticated users to read their own profile, also test that they cannot read someone else's profile.
Why it's a problem: Testing rules against production Firestore instead of the emulator, consuming free-tier quota and risking data changes
How to avoid: Always run tests against the local emulator. Use initializeTestEnvironment with host: '127.0.0.1' and port: 8080 to connect to the Firestore emulator.
Why it's a problem: Not testing rules that check existing document data (resource.data) because the test documents do not exist yet
How to avoid: Use testEnv.withSecurityRulesDisabled() to seed documents before the test, then test the actual operation with rules enabled.
Why it's a problem: Using the same test data across tests without clearing, causing tests to pass or fail based on execution order
How to avoid: Call testEnv.clearFirestore() in afterEach to reset all data between tests. This ensures complete test isolation.
Best practices
- Test every rule with both assertSucceeds (should allow) and assertFails (should deny) for complete coverage
- Seed test data with withSecurityRulesDisabled when testing rules that check existing document values
- Use descriptive test names that state the expected behavior: 'authenticated user can read own profile'
- Clear Firestore data between tests with testEnv.clearFirestore() in afterEach for isolation
- Test custom claims by passing them as the second argument to authenticatedContext()
- Run rules tests in CI/CD using firebase emulators:exec to catch rule regressions
- Create a helper function for seeding data to reduce boilerplate across tests
- Test update operations separately from create operations since they may have different rules
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to write automated tests for my Firestore security rules. Show me how to use @firebase/rules-unit-testing with the Firebase Emulator Suite to test authenticated reads, writes, unauthenticated access denial, custom claims, and data validation rules.
Create a comprehensive Firestore security rules test suite using @firebase/rules-unit-testing v3 and vitest. Include tests for user profile ownership, post creation with auth, owner-based deletion, admin access via custom claims, unauthenticated denial, and data validation. Show the seedDoc helper pattern.
Frequently asked questions
Do I need a real Firebase project to run rules tests?
No. The Firestore emulator runs entirely locally. You can use any string as the projectId in initializeTestEnvironment. No network connection or real Firebase project is required.
How do I run rules tests in CI/CD?
Use firebase emulators:exec which starts the emulators, runs your test command, and shuts down automatically. Example: firebase emulators:exec 'npx vitest run tests/firestore.rules.test.ts'.
Can I test Firestore rules without the emulator?
Not with the @firebase/rules-unit-testing library, which requires the emulator. The Firebase Console has a Rules Playground for manual testing, but it runs against production and does not support automated tests.
How do I test list operations (getDocs on a collection)?
Create a test that calls getDocs on a collection query and wrap it in assertSucceeds or assertFails. Remember that Firestore rules evaluate list access separately from get access.
What happens if my rules have a syntax error?
initializeTestEnvironment will throw an error when loading the rules file. Fix the syntax error in your firestore.rules file and re-run the tests.
How do I test rules for subcollections?
Seed the parent document first with withSecurityRulesDisabled, then test CRUD operations on the subcollection path. The path format is collection/docId/subcollection/subDocId.
Can RapidDev help write and maintain Firestore security rules tests?
Yes. RapidDev can create comprehensive security rules test suites, integrate them into your CI/CD pipeline, and help design rules that balance security with your application's access patterns.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation