Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Test Firestore Security Rules

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.

What you'll learn

  • How to set up @firebase/rules-unit-testing with the Firestore emulator
  • How to create test contexts for different auth states and custom claims
  • How to write assertSucceeds and assertFails tests for CRUD operations
  • How to seed test data and test rules that depend on existing documents
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read15-20 minFirebase CLI v13+, Node.js 18+, @firebase/rules-unit-testing v3+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1# Install dependencies
2npm install --save-dev @firebase/rules-unit-testing vitest
3
4# Create test file
5# tests/firestore.rules.test.ts

Expected result: The testing library is installed and ready for your test file.

2

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.

typescript
1import {
2 initializeTestEnvironment,
3 assertSucceeds,
4 assertFails,
5 RulesTestEnvironment
6} from '@firebase/rules-unit-testing';
7import { doc, getDoc, setDoc, updateDoc, deleteDoc, collection, getDocs } from 'firebase/firestore';
8import { readFileSync } from 'fs';
9
10let testEnv: RulesTestEnvironment;
11
12beforeAll(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: 8080
19 }
20 });
21});
22
23afterEach(async () => {
24 await testEnv.clearFirestore();
25});
26
27afterAll(async () => {
28 await testEnv.cleanup();
29});

Expected result: The test environment is initialized with your rules loaded into the local Firestore emulator.

3

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().

typescript
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 });
7
8 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 });
13
14 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 });
25
26 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.

4

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.

typescript
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 });
7
8 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});
16
17describe('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 denied
26 })
27 );
28 });
29
30 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 denied
36 authorId: 'alice'
37 })
38 );
39 });
40});

Expected result: Tests confirm that unauthenticated access is denied and data validation rules reject invalid inputs.

5

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.

typescript
1describe('Owner-based access', () => {
2 test('post author can delete their own post', async () => {
3 // Seed a post owned by alice
4 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 });
12
13 // Now test as alice — should be allowed to delete
14 const alice = testEnv.authenticatedContext('alice');
15 await assertSucceeds(deleteDoc(doc(alice.firestore(), 'posts', 'post-1')));
16 });
17
18 test('non-author cannot delete the post', async () => {
19 // Seed a post owned by alice
20 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 });
28
29 // Test as bob — should be denied
30 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.

6

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.

typescript
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 });
8
9 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 });
16
17 const admin = testEnv.authenticatedContext('admin-1', { role: 'admin' });
18 await assertSucceeds(deleteDoc(doc(admin.firestore(), 'posts', 'post-1')));
19 });
20
21 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

firestore.rules.test.ts
1// Complete Firestore security rules test suite
2// Covers user profiles, posts, admin access, and data validation
3
4import {
5 initializeTestEnvironment,
6 assertSucceeds,
7 assertFails,
8 RulesTestEnvironment
9} from '@firebase/rules-unit-testing';
10import {
11 doc, getDoc, setDoc, updateDoc, deleteDoc,
12 collection, getDocs
13} from 'firebase/firestore';
14import { readFileSync } from 'fs';
15
16let testEnv: RulesTestEnvironment;
17
18beforeAll(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: 8080
25 }
26 });
27});
28
29afterEach(async () => await testEnv.clearFirestore());
30afterAll(async () => await testEnv.cleanup());
31
32// Helper to seed data bypassing rules
33async function seedDoc(path: string, data: Record<string, unknown>) {
34 await testEnv.withSecurityRulesDisabled(async (ctx) => {
35 await setDoc(doc(ctx.firestore(), path), data);
36 });
37}
38
39describe('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 });
44
45 test('user cannot read other profile', async () => {
46 const ctx = testEnv.authenticatedContext('u1');
47 await assertFails(getDoc(doc(ctx.firestore(), 'users', 'u2')));
48 });
49
50 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 });
56
57 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});
64
65describe('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 });
71
72 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});
78
79describe('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});
85
86describe('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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.