DEV Community

MartinJ
MartinJ

Posted on • Edited on

3.2 Getting serious with Firebase V9 - Using the Firebase emulators

Last reviewed : May 2023

Introduction

When part 3.1 of this series (Moving to "ECMA Modules") introduced you to the Firebase V9 modular platform, it left you working exclusively into your production Cloud database.

This is fine while you're getting ready for your first live release, but once you've got real users and have changes to test out you need to think again.

What you need is some way of doing your testing locally. But just thinking about how you'd go about this, even with the limited range of Firebase services you've seen so far - database and rules - is probably enough to make you go weak at the knees.

Local testing is never easy to organise. In the past you would have had to sort something out entirely under your own steam - for example, you might have had to install a local SQL server. This would be hard work and would likely create some dangerous inconsistencies due to differences between your test tools and their live equivalents.

The good news is that the Firebase emulator suite creates a near-perfect local copy of Firebase production arrangements. For example, when you launch the Firestore emulator, you'll find that it looks almost exactly like the live version in the Firebase Console.

So, lets get started. As a fallback, if I'm not making things clear enough, you might find it useful to refer to Google's own documentation at Install, configure and integrate Local Emulator Suite .

Configuring the Firebase emulators

If you've previously read Project Configuration in this series, you'll find that you've actually been here before. Configuring the Firebase emulators just requires another run through Firebase init to add a few more parameters to the firebase.json file. So, open a terminal window for your project and enter the following command:

firebase init emulators
Enter fullscreen mode Exit fullscreen mode

Once you've confirmed that yes you do want to proceed, the CLI will ask you which emulators you want to use.

Because terminal windows don't respond to "point and click" instructions, selecting your emulators from the list presented is a rather cumbersome procedure. You have to proceed by using the arrow keys on your keyboard. I suggest that, for now, you concentrate on just the "Firestore" and "Hosting" emulators - ie those for which you have an immediate requirement.

So, press the down-arrow key to highlight the corresponding entries in the list and, for each, select the entry by pressing the space bar (this toggles selection on and off). Once you're done, activate your request by pressing the return key.

Accept the "default ports" offered by the CLI and also respond "y" to the offer to "download the emulators now?". The CLI should then respond with "Firebase initialization complete!".

You might find it interesting now to see what the CLI has done to your firebase.json file. This should now look something like the following:

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "emulators": {
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5000
    },
    "ui": {
      "enabled": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You should now be able to start the emulators as follows:

firebase emulators:start
Enter fullscreen mode Exit fullscreen mode

In response you'll see something like:

i  emulators: Starting emulators: firestore, hosting
!  emulators: It seems that you are running multiple instances of the emulator suite for project fir-expts-app. This may result in unexpected behavior.
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: public
+  hosting: Local server: http://localhost:5000
!  ui: Emulator UI unable to start on port 4000, starting on 4001 instead.
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4001                │
└─────────────────────────────────────────────────────────────┘

┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4001/firestore │
├───────────┼────────────────┼─────────────────────────────────┤
│ Hosting   │ localhost:5000 │ n/a                             │
└───────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.  
Enter fullscreen mode Exit fullscreen mode

You may be alarmed at this point to find that your terminal session appears to have stalled. Fret not. What has happened is that your terminal session has become a "server" for your emulator components. It's now waiting for you to start using them.

To see what's going on, cut and paste the address displayed above as View Emulator UI at http://localhost:4001 into your browser to open the following page:
Emulator launchpad

What you've seeing here is a sort of "launchpad" for the various emulator components. Just for the moment, concentrate on the "Firestore emulator" in the middle of the top row and the "Hosting emulator" to the right of the second row. The first of these launches a tool that allows you to create and administer a local Firestore database and the second gives you a quick way of starting your index.html file in the browser.

Starting with the "Firestore emulator" box, click the "Go to emulator" button at the bottom right. This will reveal a "Firestore Emulator Suite" page" that should look a bit familiar.
Firestore emulator
Yes, this is a fair replica of the procedure you used to create test data in the early days when you were using the Firestore tool in the Cloud Firebase Console.

Returning to the "launchpad" page and the "Hosting emulator" box, click the "View website" button to the bottom right. This will "serve" your index.html file as "localhost" in your browser.

Webpage launched from Firebase emulator
Take a moment to consider what's happening here. Your index.html file has been launched directly into the browser with its Firebase code in whatever state you last left it. For example, if you have now moved onto modular imports, it will be referencing the packed_index.js file you created with webpack. Even better, it runs! But, this locally hosted webapp is still connecting to your Cloud Firestore collections. We're still not quite where we want to be

Creating and operating a local Firestore database

You've now given yourself a way of testing code changes without deploying them into the live system, but this is no help if these changes make a mess of your live data. So, you now need a way to redirect the local code to a local database.

Connecting your webapp to a local database

This is surprisingly easy to do, though it does require you to make a change to your webapp's code. Just add the following line of code to the index.js file after you've set up the db variable:

connectFirestoreEmulator(db, 'localhost', 8080);
Enter fullscreen mode Exit fullscreen mode

and add the connectFirestoreEmulator function to your firestore import

But now the question is "how do you get some data into this local database?"

Creating and populating a local database

Although you've already seen that the emulator gives you a familiar arrangement for defining and populating collections, you may still have grim memories of trying to do this through the live Firebase console. It's not going to be any easier here. In such a situation you'll most likely be looking for a Firebase export/import facility to haul a suitable dataset out of your live database.

Sadly, I'm afraid that, at least at the time of writing (Nov 2021), you will look in vain. There is a Cloud database export facility but it's designed for Cloud backup and it's not going to be at all helpful in providing you with a local extract file.

While you're still getting your eye in, I suggest you just add a temporary bit of initialisation code to your application. You can easily comment it out when you want to revert your code for live operation. Here's an example of what you might write:

async function buildTestData() {
    let collectionData = [
        {
            "userEmail": "ab@gmail.com",
            "userPurchase": "chocolate"
        },
        {
            "userEmail": "ab@gmail.com",
            "userPurchase": "firelighters"
        }
    ]


    for (let i = 0; i < collectionData.length; i++) {
        let collRef = collection(db, "userShoppingLists");
        let docRef = doc(collRef);
        await setDoc(docRef, collectionData[i]).catch((error) => {
            alert("Error in buildTestData " + error)
        });
    } 
}
Enter fullscreen mode Exit fullscreen mode

It shouldn't be too difficult to invent ways of obtaining more elaborate jsons from your production collections. For example you might add a hidden button to your live webapp to print out a json that you can then copy and paste into your code via the clipboard.

If you're still determined to find more serious ways of importing a collection from your production Cloud Firestore database, you might look at How to import data from cloud firestore to the local emulator? on Stackoverflow.

Building a library of Test data sets

Once you've finally succeeded in your bid to create test collections in your local database, you are likely to feel distinctly unhappy when you restart the emulators and find that your data has disappeared! Yes, unless directed otherwise, the Firestore emulator always starts with a clean sheet.

Actually, when you think about it, this is quite a sensible idea. When you're doing serious testing, you'll want your tests start with a clean database served up from a standard data set.

To ask the emulator to save your data collections after you've just created them you just issue a one-off command like:

firebase emulators:export ./[collection_backup]
Enter fullscreen mode Exit fullscreen mode

where [collection_backup] is a folder name that you choose yourself. If the [collection_backup] folder doesn't exist, firebase will create it.

To restart the emulators with a copy of this data you would then use the following command:

firebase emulators:start --import=./[collection_backup]
Enter fullscreen mode Exit fullscreen mode

Management of this arrangement for a serious project is clearly going to be an issue,- I'm sure that by now you'll have realised that you need to be quite organised if you're to use the emulators effectively.

Precise arrangements beyond this are clearly going to differ from project to project. But you probably need to:

  1. Plan for an initial test-collection initialisation phase to set up test data sets in a series of [collection_backup] folders.
  2. Perhaps consider creating a corresponding series of "run_emulation_for_[collection_backup].ps1" scripts to systematise the running of a particular test against the correct data set.
  3. Plan procedures to ensure that the code changes introduced to rig your source code for testing can't leak into the live system. These arrangements will obviously need to fit into whatever procedures you choose to adopt for version management and are beyond the scope of this particular post.

Yes, serious IT is hard!

Applying Rules in the Emulation environment

You'll note that while the "Firestore Emulator Suite" page offered facilities to manage collections it said nothing about rules. In the emulation environment you need to specify these separately in a firestore.rules file (stored at the project root) and provide a pointer to this file in your firebase.json. This is easy enough to organise - just cut and paste from your live rules page into your firestore.rules file and add the following to your firebase.json:

    "firestore": {
      "rules": "firestore.rules"
    },
Enter fullscreen mode Exit fullscreen mode

To run the example code provided in Coding a simple webapp - the final version with a user-login - my firestore.rules file contained the following:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /userShoppingLists/{document} {
        allow read, write : if true;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that when you switch back to production deployment, a bare firebase deploy command will upload your firestore.rules file along with everything else to the Cloud. If subsequently you use the Firestore console to make changes directly into the hosted system, it's quite easy to forget that any subsequent firebase deploy wil overwrite these changes with the content of firestore.rules - you need to change these too.

The next post in this series Background processing with Cloud Functions describes Firebase facilities for handling processing requirements that aren't natural candidates for browser operation. You'll find that the skills you've acquired with the emulators come in very handy here because they make development in this area a whole lot easier.

Appendix- using VSCode terminal windows

If you're unfamiliar with VSCode terminal window, here are a few notes that you might find useful:

  1. To open a new terminal window in VSCode, click "terminal/new terminal" on the menu bar and select the folder (project) that you want the session to focus on. You can open multiple windows and, in fact, you will need to open multiple terminals because once you've started the Firebase emulators in a terminal window this will be locked to their server. Meanwhile, you will still need to issue other commands (eg firebase emulators:export ./collection_backup to backup a collection). You can toggle between terminal sessions using the drop-down list at the head of the terminal window. If you've hidden all your terminal windows, you can reopen the latest one by clicking "view/terminal".

  2. When you start a new terminal it will be opened with the default shell (powershell is a popular choice), but once you've started the emulators, you'll find the terminal type has switched from "powershell" to "node". This is because the original terminal session has started a node server to deliver the emulator functions. To stop the emulator, use ctrl C, but note that this will likely leave a java service running and clinging onto a port. You can clear this with the command taskkill /f /im java.exe.

  3. You can ask a terminal to rerun the last command you gave it by pressing the up-arrow key. Pressing the up-arrow twice will repeat the last but one and so on.

  4. The "Run Active File" trick described in Post 3.1 as a convenient way of running the contents of a script is worth re-visiting.

Other posts in this series

If you've found this post interesting and would like to find out more about Firebase you might find it worthwhile having a look at the Index to this series.

Top comments (0)