# How to Test Firestore Rules Locally

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Firebase CLI v13+, Node.js 18+, @firebase/rules-unit-testing v3+
- Last updated: March 2026

## TL;DR

To test Firestore security rules locally, install the Firebase Emulator Suite, start the Firestore emulator with firebase emulators:start, and connect your app or test scripts to the local emulator instead of production. Use the Rules Playground in the Emulator UI at localhost:4000 for quick manual tests, or write automated test suites using the @firebase/rules-unit-testing library with assertSucceeds() and assertFails() to verify that your rules allow and deny access correctly.

## Testing Firestore Security Rules with the Local Emulator

Testing security rules against production Firestore is slow, risky, and burns through free quota. The Firebase Emulator Suite lets you run Firestore locally with your rules file, test access patterns instantly, and iterate on rules without deploying. This tutorial covers setting up the emulator, using the Rules Playground for interactive testing, writing automated test suites, and connecting your development app to the local emulator.

## Before you start

- Firebase CLI installed (npm install -g firebase-tools)
- A Firebase project initialized with firebase init (select Firestore and Emulators)
- A firestore.rules file in your project root
- Java JDK 11+ installed (required by the Firestore emulator)

## Step-by-step guide

### 1. Initialize the emulator suite in your project

Run firebase init emulators and select the Firestore emulator. The CLI adds emulator configuration to your firebase.json including the port numbers. The default Firestore emulator port is 8080 and the Emulator UI runs on port 4000. You can customize these ports in firebase.json if they conflict with other services.

```
# Initialize emulators (select Firestore when prompted)
firebase init emulators

# Your firebase.json will include:
# {
#   "emulators": {
#     "firestore": { "port": 8080 },
#     "ui": { "enabled": true, "port": 4000 }
#   }
# }
```

**Expected result:** firebase.json is updated with emulator configuration. The Firestore emulator is ready to start.

### 2. Start the emulators and open the Rules Playground

Start all configured emulators with firebase emulators:start. The CLI loads your firestore.rules file and starts the Firestore emulator on port 8080 and the Emulator UI on port 4000. Open localhost:4000 in your browser to access the Emulator UI which includes a Firestore data viewer, Auth user manager, and the Rules Playground for interactive rule testing.

```
# Start all emulators
firebase emulators:start

# Or start only the Firestore emulator
firebase emulators:start --only firestore

# With data persistence between restarts
firebase emulators:start --import=./emulator-data --export-on-exit=./emulator-data
```

**Expected result:** The terminal shows the Firestore emulator running on port 8080 and the Emulator UI is accessible at http://localhost:4000.

### 3. Test rules interactively with the Rules Playground

In the Emulator UI at localhost:4000, navigate to Firestore and click the Rules Playground tab. The playground lets you simulate read and write requests against your rules without writing any code. Select the operation type (get, list, create, update, delete), enter the document path, optionally set an authenticated user context, and provide request data for writes. The playground shows whether the request was allowed or denied and highlights the specific rule that matched.

**Expected result:** The Rules Playground shows ALLOW or DENY for each simulated request, with the matching rule highlighted in your rules file.

### 4. Install the rules-unit-testing library for automated tests

Install @firebase/rules-unit-testing as a dev dependency. This library provides helper functions to create test contexts with or without authentication, and assertion functions to verify that operations succeed or fail as expected. It connects directly to the running Firestore emulator.

```
# Install the testing library
npm install --save-dev @firebase/rules-unit-testing

# Also install a test runner if you don't have one
npm install --save-dev vitest
```

**Expected result:** The @firebase/rules-unit-testing package is installed and ready to use in your test files.

### 5. Write automated tests for your Firestore rules

Create a test file that initializes the testing environment, sets up test contexts for authenticated and unauthenticated users, and uses assertSucceeds() and assertFails() to verify rule behavior. Each test should check a specific access pattern: can an authenticated user read their own profile? Can they read someone else's profile? Can an unauthenticated user create a post?

```
// tests/firestore-rules.test.ts
import {
  initializeTestEnvironment,
  assertSucceeds,
  assertFails,
  RulesTestEnvironment
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore';
import { readFileSync } from 'fs';

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'test-project',
    firestore: {
      rules: readFileSync('firestore.rules', 'utf8'),
      host: '127.0.0.1',
      port: 8080
    }
  });
});

afterEach(async () => {
  await testEnv.clearFirestore();
});

afterAll(async () => {
  await testEnv.cleanup();
});

test('authenticated user can read their own profile', async () => {
  const alice = testEnv.authenticatedContext('alice');
  const db = alice.firestore();
  await assertSucceeds(getDoc(doc(db, 'users', 'alice')));
});

test('user cannot read another user profile', async () => {
  const alice = testEnv.authenticatedContext('alice');
  const db = alice.firestore();
  await assertFails(getDoc(doc(db, 'users', 'bob')));
});

test('unauthenticated user cannot create a post', async () => {
  const unauth = testEnv.unauthenticatedContext();
  const db = unauth.firestore();
  await assertFails(
    setDoc(doc(db, 'posts', 'post-1'), { title: 'Test', authorId: 'anon' })
  );
});
```

**Expected result:** Running the test suite with vitest confirms that your rules correctly allow and deny access for each test case.

### 6. Connect your development app to the Firestore emulator

During development, connect your app to the local Firestore emulator instead of production. Call connectFirestoreEmulator() after initializing Firestore. This redirects all Firestore operations to the local emulator. Only do this in development — never in production. A common pattern is to check an environment variable to decide whether to connect to the emulator.

```
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { initializeApp } from 'firebase/app';

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);

// Connect to emulators in development only
if (import.meta.env.DEV) {
  connectFirestoreEmulator(db, '127.0.0.1', 8080);
  connectAuthEmulator(auth, 'http://127.0.0.1:9099');
  console.log('Connected to Firebase emulators');
}
```

**Expected result:** Your development app reads and writes to the local Firestore emulator. Changes appear in the Emulator UI at localhost:4000.

## Complete code example

File: `firestore-rules.test.ts`

```typescript
// Complete Firestore rules test suite
// Tests user profiles, posts, and admin access patterns

import {
  initializeTestEnvironment,
  assertSucceeds,
  assertFails,
  RulesTestEnvironment
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc, updateDoc, deleteDoc } from 'firebase/firestore';
import { readFileSync } from 'fs';

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'rules-test',
    firestore: {
      rules: readFileSync('firestore.rules', 'utf8'),
      host: '127.0.0.1',
      port: 8080
    }
  });
});

afterEach(async () => {
  await testEnv.clearFirestore();
});

afterAll(async () => {
  await testEnv.cleanup();
});

// --- User profile rules ---

test('user can read own profile', async () => {
  const ctx = testEnv.authenticatedContext('user1');
  await assertSucceeds(getDoc(doc(ctx.firestore(), 'users', 'user1')));
});

test('user cannot read other profiles', async () => {
  const ctx = testEnv.authenticatedContext('user1');
  await assertFails(getDoc(doc(ctx.firestore(), 'users', 'user2')));
});

test('user can create own profile with member role', async () => {
  const ctx = testEnv.authenticatedContext('user1');
  await assertSucceeds(
    setDoc(doc(ctx.firestore(), 'users', 'user1'), {
      email: 'user1@test.com',
      displayName: 'User One',
      role: 'member'
    })
  );
});

test('user cannot create profile with admin role', async () => {
  const ctx = testEnv.authenticatedContext('user1');
  await assertFails(
    setDoc(doc(ctx.firestore(), 'users', 'user1'), {
      email: 'user1@test.com',
      displayName: 'User One',
      role: 'admin'
    })
  );
});

// --- Unauthenticated access ---

test('unauthenticated user cannot read profiles', async () => {
  const ctx = testEnv.unauthenticatedContext();
  await assertFails(getDoc(doc(ctx.firestore(), 'users', 'user1')));
});

test('unauthenticated user cannot write posts', async () => {
  const ctx = testEnv.unauthenticatedContext();
  await assertFails(
    setDoc(doc(ctx.firestore(), 'posts', 'post1'), {
      title: 'Hack',
      authorId: 'anon'
    })
  );
});
```

## Common mistakes

- **Forgetting to start the Firestore emulator before running rules tests, causing connection refused errors** — undefined Fix: Always run firebase emulators:start in a separate terminal before running your test suite. Add a script like "test:rules": "firebase emulators:exec 'vitest run tests/firestore-rules.test.ts'" to run them together.
- **Not calling testEnv.clearFirestore() between tests, causing test data from one test to affect another** — undefined Fix: Add testEnv.clearFirestore() in an afterEach block to reset the emulator data between every test for isolation.
- **Using localhost instead of 127.0.0.1 for the emulator host, causing connection failures on systems with IPv6 enabled** — undefined Fix: Always use 127.0.0.1 as the emulator host in both your test configuration and your app's connectFirestoreEmulator call.

## Best practices

- Run rules tests as part of your CI/CD pipeline using firebase emulators:exec to start emulators, run tests, and shut down automatically
- Test both positive cases (should succeed) and negative cases (should fail) for every rule
- Use testEnv.clearFirestore() in afterEach to ensure test isolation
- Keep your firestore.rules file in version control and review rule changes in pull requests
- Test edge cases like empty fields, missing auth, and boundary values for data validation rules
- Use the Rules Playground in the Emulator UI for quick manual checks while developing rules
- Test with the Auth emulator running so request.auth is properly populated in rules

## Frequently asked questions

### Do I need Java installed to run the Firestore emulator?

Yes. The Firestore emulator requires Java JDK 11 or higher. Install it from adoptium.net or via your package manager. The Firebase CLI will prompt you if Java is missing.

### Can I test rules without starting the full emulator suite?

You can use the Rules Playground in the Firebase Console (production) for quick manual tests, but this counts toward your free-tier quota. For local testing, the emulator is required.

### How do I test rules that depend on existing document data?

Use testEnv.withSecurityRulesDisabled() to seed data before your test. This bypasses rules to create the initial state, then your actual test runs with rules enabled against that data.

### Do emulator rules hot-reload when I change the firestore.rules file?

Yes. The emulator watches your firestore.rules file and automatically reloads rules when the file changes. You do not need to restart the emulator after editing rules.

### Can I run rules tests in CI/CD without a Firebase project?

Yes. The emulator does not require a real Firebase project. Use any string as the projectId in initializeTestEnvironment. The tests run entirely locally against the emulator.

### How do I test custom claims in security rules?

Use testEnv.authenticatedContext('uid', { role: 'admin' }) to create a test context with custom claims. The second argument becomes request.auth.token in your rules.

### Can RapidDev help set up a testing pipeline for Firestore security rules?

Yes. RapidDev can implement automated security rules testing with the Firebase Emulator Suite, integrate it into your CI/CD pipeline, and ensure comprehensive coverage of your access control patterns.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-test-firestore-rules-locally
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-test-firestore-rules-locally
