Firebase Emulation

When testing your web app, whether by hand or with automated testing code, you do not want the tests to use your production database. You certainly don't testing code, which might have bugs, to write data on the same database your users are using. You also probably want to control what's in the database to make sure it has specific data situations needed for a test, such as users with and without some preference setting.

Similarly, if automated tests need to authenticate in order to test parts of your app, you don't want to store user names and passwords -- real or fictional -- in test code.

With unit test frameworks, you can mock the code that calls the database, but an end-to-end tester like Cypress interacts with the app as a black-box. Cypress does provide a way to catch simple network calls to third-party APIs, such as Yelp or Spotify, with intercept(), but this does not work with services like Firebase that use network sockets to communicate.

For this reason, Firebase provides emulators that you can install and run. The emulators behave just like the real Firebase services but are run on your own machine. These emulators are Java programs so you need to have a Java runtime installed. There are emulators for all the major Firebase services. Similarly, Microsoft provides emulator for their Azure Cosmo DB, and Amazon provides an emulator for its LAMBDA cloud function service.

The key steps to using the Firebase emulators are:

  • Install Java if not present
  • Install the emulators
  • Modify your code to use the emulators when testing

Install Java

The emulators are Java applications. To see if you have Java, run the following in a terminal window:

java -version

You need at least version 11 of Java. If you don't have it, you can get it from many places. Oracle has gone back and forth on how open their Java is. I recommend Adoptium.

Install emulators

You install the emulators you need with firebase init.

firebase init emulators

This will ask you which emulators you need. The most common options are:

  • Realtime Database -- most apps need this
  • Authentication -- most apps need this
  • Firestore -- needed if you used Firestore for structured documents
  • Cloud Storage -- needed if you used Cloud Storage to store pictures and such
You do not need the hosting emulator. Just use the normal local React development server to locally run your app.

Firebase will ask if you want to download the emulators. You can do this now, or have Firebase do this when the emulators are first run.

Test your setup by running

firebase emulators:start

This downloads and starts the emulators. It may take a minute. Firebase will display links for the web user interfaces for the emulated services you are running. Open those links to verify the emulators are working.

If you get a timeout opening the links, and you are using Node 17, you probably need to replace all occurrences of localhost in the file firebase.json to 127.0.0.1. The final results should look like this:

{
  "emulators": {
    "auth": {
      "port": 9099,
      "host": "127.0.0.1"
    },
    "database": {
      "port": 9000,
      "host": "127.0.0.1"
    },
    "ui": {
      "enabled": true
    }
  }
}

If you have other emulators installed, update their entries the same way.

Add emulator scripts

To make it easy to run the emulators for testing, add the following scripts to your package.json. You can give them any name you like. Here are some scripts for working with the Firebase emulators.

{
  ...
  "scripts": {
    ...
    "em:start": "firebase emulators:start --import=./saved-data --export-on-exit",
    "em:exec": "firebase emulators:exec --import=./saved-data 'npm start'",
    "em:execui": "firebase emulators:exec --ui --import=./saved-data 'npm start'"
  }
}

Use npm run em:start to run the emulators and start a Firebase-like web console where you can add users or data. You can use the console to create your test data and test users, as shown below. The options --import=./saved-data --export-on-exit tell the emulators to save any changes you make in the directory ./saved-data when you stop the emulators.

Use npm run em:exec or npm run em:execui to start the emulators and then your app, for testing your app locally. Changes made to data are NOT saved. The only difference is that execui also starts the web console emulator so you can see what changes your code is making to data.

If you are using some other database, like mySQL or MongoDB, then you will need to write a more complicated script to start your local database and import the necessary testing data. It's usually clearest to define a separate script to start your database, and then call that script in em:start and em:exec.

Create test data

Start the emulators with

npm run em:start

This will start the emulators and a local Firebase console you can use this to initialize your test data.

  • Open http://localhost:4000 in your browser. You should see a Firebase console.
  • On the console page, click on the button for Authentication. Create whatever sample users you need for testing. Firebase will create local user IDs for them.
  • On the console page, click on the button for the Realtime Database. Create or import whatever test data you need for testing your app. If some data needs to be stored under a user ID, use the IDs given on the Authentication page.
  • In your terminal shell where you started the emulators, stop them with control-C. This will trigger a graceful shutdown and save your data to the directory specified in the script.

Test! Do the above steps to create some relevant sample data for your app. Stop the emulator. Verify that files have been created in the directory saved-data in your repository. Those files are required for the steps belows. If you do not see saved data, check the log files in the repository that the emulators generate for error messages, such as firebase-debug.log, database-debug.log, and so on.

Avoid running two sets of emulators at once. This can lead to changes not being saved. For example, if you run em:start, be sure to exit with control-C before running any tests that call em:exec.

For example, to create a test database for the Quick React Scheduler app:

  • Start the database and authentication emulators.
  • In the local authentication web console, create a test user, e.g., "Test User" with email address "tester@gmail.com"
  • In the local realtime database web console, import the sample course data
  • Edit the schedule title to "Test CS Courses" so that it's easy to know when test data is being used
  • Stop the emulators with control-C to save the changes.

Adapt app to use emulators

Now we need to change the code that sets up Firebase to use the emulators. Suppose our app uses the Realtime Database and Firebase Authentication, and sets things up in firebase.js like this:

import { getAuth, GoogleAuthProvider, signInWithCredential } from "firebase/auth";
import { getDatabase } from "firebase/database";

const firebaseConfig = { 
  ...
};

const firebase = initializeApp(firebaseConfig)
const auth = getAuth(firebase);
const database = getDatabase(firebase);
...

We want to add code to tell Firebase when to use the emulators. We don't want to do this when the deployed web app is running in production. If you use Vite, you test if an app is being run in development mode with the JavaScript expression

import.meta.env.MODE === 'development'

You use this expression in your app code that initializes Firebase. If the app is being run in development, then your initialization code should connect to the appropriate emulators. The Firebase libraries export functions to connect to emulators. Each functions is called a little differently so you have to study the documentation examples carefully.

For example, here's the Firebase initialization code from above, changed to test if it is being run outside of production. If so, it connects to the emulators for the realtime database and authentication. It then logs in a test user previously created and saved in the local emulator database.

import { connectAuthEmulator, getAuth, GoogleAuthProvider } from "firebase/auth";
import { getDatabase, connectDatabaseEmulator } from "firebase/database";

const firebaseConfig = { 
  ...
};

const firebase = initializeApp(firebaseConfig)
const auth = getAuth(firebase);
const database = getDatabase(firebase);

if (!globalThis.EMULATION && import.meta.env.MODE === 'development') {
  connectAuthEmulator(auth, "http://127.0.0.1:9099");
  connectDatabaseEmulator(database, "127.0.0.1", 9000);

  signInWithCredential(auth, GoogleAuthProvider.credential(
    '{"sub": "qEvli4msW0eDz5mSVO6j3W7i8w1k", "email": "tester@gmail.com", "displayName":"Test User", "email_verified": true}'
  ));
  
  // set flag to avoid connecting twice, e.g., because of an editor hot-reload
  globalThis.EMULATION = true;
}

This uses the test user created manually earlier. The sub field holds the unique ID generate by Firebase in the user database.

Test emulation

If all is working correctly, then the following command should start your app locally with emulators.

npm run em:exec

You should see the test data. You should be able to change the data. If you stop and re-start you should see the original test data.

If any emulator fails to start, Firebase will shut down all of them. One common message is

Port 9000 is not open on localhost, could not start Database Emulator

This usually means a previous shutdown of the database emulator didn't kill all necessary processes. See this link for how to find and kill the previous process. Then try again.

Port 5000 is in use

If Firebase can't start localhost because port 5000 is in use, that means you installed and ran the hosting service. Remove the entry for hosting from ./firebase.json and try again.

If no specific reason is given, look in the debugging logs that Firebase creates when the emulators fail to start. They will be in your app directory, usually in the same directory that has firebase.json. Look for files with names like firebase-debug.log and database-debug.log.

© 2024 Chris Riesbeck
Template design by Andreas Viklund