We ❤️ Open Source
A community education resource
Testing Firestore security rules without touching production
Learn how to use Firebase emulators and testing libraries to validate your security rules before deployment.
Firestore makes it extremely easy to access data directly from your applications. There are libraries ready for most major languages. That flexibility comes at a cost, however. Since the applications can modify and read the database directly, you need to think about your database like a public API. Thankfully, we can write security rules to enforce access restrictions on the data. This is important for privacy, but also making sure the data is only modified in the ways we want to allow.
I’ve used Firebase now for multiple projects and love how quickly I can get projects moving, but eventually I’ll need more complex security rules. Developing these directly in the Firebase console is not a great option. I want to know that changes that I’ve made aren’t going to break existing functionality. I’m also not a huge fan of running tests against production data.
While Firebase itself isn’t open source software, the Firebase team has released an open source NPM testing module we can use for testing against our security rules. It can be used in existing Javascript tests or we can set up an independent project solely for testing. I’d normally prefer to keep the language the same as my development language (Dart with Flutter), but the only library provided at this time is for Node.
A complete guide to Firestore security rules testing setup
What we’ll do is set up an independent testing project that can be used regardless of what language the actual project is setup in. We’ll need to add emulator support to our existing firebase project. Then we can begin adding tests. We’ll add some test data, and then another test that mocks an authenticated user. The components of those tests can then be used as needed to make additional tests in your project.
Before these steps will work for you, you’re going to need the following installed on your machine:
- NodeJs
- NPM
- Java
- firebase-tools NPM package
For these tests, we’ll use a simple example schema for a messaging app:
Messages { sender: string, recipients: string[], content: string }
Set up the Firestore emulator
If you have already set up the emulator for your project, then you can skip ahead. To setup the emulators, you’ll need to run:
> firebase init
Select Emulators from the list and then select the Firestore Emulator. The rest of the settings can be left at their defaults. Let it download the emulators and then you’re ready to start them:
> firebase emulators:start
Once it’s up and running, you can access it at http://localhost:4000/. If you click on the Firestore tab, you’ll see an interface similar to what you get in the Firebase console for the project:

Create your test project
Now that the emulator is up and running properly, we’ll create the test project. I’m going to create it in a subdirectory of my existing project:
> mkdir rules_testing
> cd rules_testing
> npm init
Go ahead and set up the test command to “mocha –exit tests/*<em>/</em>.spec.js” and make sure to set the project type to “module.”
mocha --exit tests/**/*.spec.js

Now we’ll install our dependencies:
> npm install --save-dev mocha @firebase/rules-unit-testing
These tests will assume that your firestore rules are being stored at the root of your existing project in your firestore.rules file. Here’s what the one from this project looks like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /messages/{messageId} {
allow read, write: if request.auth.uid != null &&
(request.auth.uid in resource.data.recipients || request.auth.uid == resource.data.sender);
}
}
}
Nothing too complicated here. By default, access is denied to any document, and then we are allowing access to message documents only if the authenticated user is either the sender or the recipient of the message. Let’s start adding some tests now!
Start adding tests (and data)
We’ll start by making sure that someone who is not authenticated can’t read a message. Create your first test in tests/messages.spec.js:
import { describe, it } from 'mocha';
import { assertSucceeds, assertFails, initializeTestEnvironment } from '@firebase/rules-unit-testing';
import fs from 'fs';
const projectId = '<<replace with your project id>>';
describe('Firestore Rules', () => {
it('should NOT allow unauthenticated users to read messages', async () => {
const testEnv = await initializeTestEnvironment({
projectId: projectId,
firestore: {
rules: fs.readFileSync('../firestore.rules', 'utf8'),
host: 'localhost',
port: 8080
},
});
const publicUser = testEnv.unauthenticatedContext();
await assertFails(publicUser.firestore()
.collection('messages')
.doc('1').get());
});
});
The general flow for writing these tests is to start by initializing the testing environment, and then setting up the context for the test to run in (authenticated or unauthenticated). We’ll use the project id from Firebase (might look like ‘my-project-ab20e‘). Then we’ll be sure to pass the rules in so they can be evaluated. For this test, we’ll make sure to use the unauthenticatedContext.
Run npm test and the it will pass:

But we can’t be sure it’s passing for the right reasons. Right now, that data doesn’t exist, so reading the data could be failing because of missing data. We’ll need to add data for the tests. We can do this manually, but to make testing easier, we can do this through the testing library.
The library provides a way for us to get around the security rules for the purposes of test setup. This is convenient since you may need to preload data that isn’t writable per your security rules. The withSecurityRulesDisabled method allows us to provide a callback which is run in a context where the security rules are disabled:
before('Re-load data', async () => {
const testEnv = await initializeTestEnvironment({
projectId: projectId,
firestore: {
rules: fs.readFileSync('../firestore.rules', 'utf8'),
host: 'localhost',
port: 8080
},
});
await testEnv.withSecurityRulesDisabled(async (context) => {
await context.firestore().collection('messages').doc('1').set({
content: "I'm a message!",
recipients: ['my_user'],
sender: ''
});
});
});
If you rerun the test, it should pass. Now you can verify now that the data exists by looking at the emulators UI:

We know data exists but the write fails because the firestore rules will not allow the write to work. This is great, but now we want to ensure that the rules will not prevent access to legitimate users. Let’s add a test to be sure that the recipient can read their messages:
it('should allow authenticated users to read their received messages', async () => {
const testEnv = await initializeTestEnvironment({
projectId: projectId,
firestore: {
rules: fs.readFileSync('../firestore.rules', 'utf8'),
host: 'localhost',
port: 8080
},
});
const myUser = testEnv.authenticatedContext('my_user');
await assertSucceeds(myUser.firestore()
.collection('messages')
.doc('1').get());
});
Run npm test and check the results:

Excellent! Obviously not a comprehensive test suite yet, but this provides a good starting point. We’re able to interact with the database as either a known user or as an unauthenticated user. We can even modify the database outside of the rules as needed for test setup. Firestore security rules have a number of powerful features, including a number of math and set operations. With a test suite in place, you are now prepared to begin iterating on your rules locally as well.
You can see the full demo at https://github.com/chris-barile/demo-firestore-rules-testing
More from We Love Open Source
- Usage rules: Making AI coding tools accessible to everyone
- How curiosity, Kubernetes, and community shaped my open source journey
- Introduction to Falco and how to set up rules
- Detecting vulnerabilities in public Helm charts
- Deep dive into the Model Context Protocol
The opinions expressed on this website are those of each author, not of the author's employer or All Things Open/We Love Open Source.