Getting started with Firebase emulators suite for Android

Do you know a friend/colleague who tests their application directly in the production environment?

Yeah, that’s a bad practice. We should have a local or staging environment where we test our code before we push to the production. That’s where the Firebase emulator comes into the picture. Firebase emulator is a tool that helps us to test various firebase services in our local environment altogether.

What is Firebase Emulator?

According to the official documentation

The Firebase Local Emulator Suite consists of individual service emulators built to accurately mimic the behavior of Firebase services. This means you can connect your app directly to these emulators to perform integration testing or QA without touching production data.

Currently, there are 5 services available in the emulator.

  1. Cloud Firestore
  2. Realtime database
  3. Cloud Functions
  4. Hosting
  5. Cloud Pub/Sub

Note: However your app will still continue to communicate with production Firebase services when emulators are not available or configured.

Why do we need Firebase Emulator? What problem does it solve?

We need the Firestore emulator to safely read and write documents in testing. Since all services are connected to the local environment, these trigger the events in each other’s services.

For example, The realtime database writes may trigger the cloud functions in the emulator.

Because of a lack of testing we face the following issues.

1. Time Consuming

No one writes a perfect code in the first attempt. It’s an iterative process. We need a fast feedback loop. Let’s say that if we are writing a cloud function without an emulator, then every time we do a change we need to push to the server which will take around 20-30 seconds which is a waste of time, causes distraction, and hamper the productivity.

2. Debugging

While developing we make a lot of assumptions and mistakes which are fixed by debugging. Without a local setup, debugging directly on the server is a nightmare. No, having console.log() in each line of the code is not a good solution either. Having a local environment allows us to debug more effectively using our favorite IDE.

3. Data Modeling

The most FAQ on firebase is about data modeling for the database. We keep experimenting with the structure of the database and if we don’t have a local environment then it’s become very difficult to define a clear boundary between the test data and the production data.

The chances of messing up the production data are very high, by doing a typo, using the wrong key, or deleting a document by mistake. 

There is one workaround for this which I am going to talk about in the next section.

4. Integration testing using CI/CD

Each individual product emulator in the Emulator Suite responds to SDK and REST API calls just like production Firebase services. So we can use our own testing tools integrating into our CI/CD to write self-contained integration tests that use the Local Emulator Suite as the backend.

5. Cost Real Money

The first thing we need to check for any third-party services is “pricing”. I hope you have already checked for the firebase. If not then please visit pricing.

The pricing won’t cause any problem if you are working on a project as a hobby which does not have any real users. But if you plan for the real customers/users then you will reach a point where you have let say, 10K users. Due to the increase in the number of users, the usage of services will grow proportionally.

Realtime DB will have a thousand simultaneous connections. Firestore will have more read/write/deletes. The cloud function will have high invokes which eventually cost you a lot of money.

So we should be aware of any unnecessary reads and downloads from the firebase especially while testing because we used to push all kinds of junk data to test various scenarios.

I recommend watching this video for firestore pricing.

For Hindi:

6. Work-around with Multiple Projects

Since we have one database per project most developers create multiple projects to handle production and test environments separately.

For example, I will create a Firebase project with the name “My Prod App” adding my app with package com.burhanrashid52.firebase.sampleapp, and then I’ll create another project called “My Test App” with package name com.burhanrashid52.firebase.sampleapp.testing.

This will help to avoid the issue because firebase pricing is project-based, not account-based.

How to get started?

When I was writing the first draft of this section. I wrote the entire step by step command in order to set up the firebase emulator suite, which firebase documentation already has. So why reinvent the wheel? Please go through the official documentation for setting this up. If you are a person who understands things more visually. Please check out this video.

Here is my short version of that steps with given commands:

  1. Setup the CLI → npm install -g firebase-tools
  2. Login to the firebase account → firebase login
  3. Select the project.
  4. Initialize firebase for the project → firebase init
  5. Select the products you need.
  6. Start the emulator → firebase emulators:start
    • (Optional) Rum specific feature using —only
    • Firestore: firebase emulators:start --only firestore
    • RealtimeDB : firebase emulators:start --only database
    • Cloud Functions : firebase emulators:start --only functions

7. Run localhost:4000 in the browser and you will see an emulator suite console.

More details here

The dashboard displays the status(On/Off) of each service emulator with a port number. We connect our client app to those port numbers. (More on this later).

We can also change the port if any of the default ports are occupied.

Connect Android (client) to firebase emulator

Our firebase emulator suite is up and running. Now we need to connect our client to the emulator suite. After that our client will be able to push and pull the data of this emulator and we can observe the changes on the emulator UI page.

I am using Android as the client here. If you are using the iOS or Web as a client then check out this official document.

Since we run android on the “android emulator” we cannot use localhost:8080 as a host. We need to use 10.0.2.2:8080 as the “android emulator” IP. We can test that by running 10.0.2.2:8080 in the android emulator browser and verify that firebase emulator UI is displayed in the android emulator browser.

Following are code snippets to set the host. The port is different for each service.

1. Firestore for Android

    val instance = FirebaseFirestore.getInstance()
    val settings = FirebaseFirestoreSettings.Builder()
            .setHost("10.0.2.2:8080")
            .setSslEnabled(false)
            .setPersistenceEnabled(false)
            .build()

    instance.firestoreSettings = settings

2. Realtime database for Android

For realtime DB we need to add a project-id in the URL. We can find this in the firebase project setting or directly copy from the realtime database tab from the firebase emulator UI.

val instance = FirebaseDatabase.getInstance("http://10.0.2.2:9000?ns=projectId")

3. Cloud Function for Android

In case of manually calling cloud function from the client.

val instance = FirebaseFunctions.getInstance()
instance.useEmulator("10.0.2.2", 5001)

We just need to make sure that we operating on the created instance. Otherwise it will get connect to the production firebase db. A better practice would be to injecting a singleton class which provide the same instance throughout the application.

4. Dependency Injection using Koin

We are using koin module and making all the dependency as a singleton using single {} Lamba and injecting that same instance where it required using

val firebaseModule = module {

    single {
        val instance = FirebaseFirestore.getInstance()

        val settings = if (isFirebaseLocal) {
            FirebaseFirestoreSettings.Builder()
                    .setHost("10.0.2.2:8080")
                    .setSslEnabled(false)
                    .setPersistenceEnabled(false)
                    .build()
        } else {
            FirebaseFirestoreSettings.Builder()
                    .setSslEnabled(false)
                    .setPersistenceEnabled(false)
                    .build()
        }
        instance.firestoreSettings = settings
        instance

    }

    single {
        if (isFirebaseLocal) {
            FirebaseDatabase.getInstance("http://10.0.2.2:9000?ns=projectId")
        } else {
            FirebaseDatabase.getInstance()
        }
    }

    single {
        val instance = FirebaseFunctions.getInstance()
        if (isFirebaseLocal) {
            instance.useEmulator("10.0.2.2", 5001)
        }
        instance
    }
}

var isFirebaseLocal = false

We used a flag isFirebaseLocal and set at MyApp class. It helps to toggle between local and production database. We can put this flag in the gradle setting as well.

We can inject using Koin.

val firestore by inject<FirebaseFirestore>()

5. Release builds

For release build, I always make this flag as false. For that, I’ve done something like this. So that I don’t accidentally put true for the release build. There are better ways to do this. But for simplicity, I am going with this.

class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) {
            isFirebaseLocal = false
        }
        // Start Koin
        startKoin {
            androidLogger()
            modules(firebaseModule)
        }
    }
}

Key things to remember.

  1. Double-check your host IP and port on a client-side. I wasted 3 hours of debugging on “why my client app is not connecting to the emulator suite”.
  2. Firebase emulator does not persist data locally. It clears all the data when we restart the server. A similar action can be achieved by clicking on the clear data button on firestore tab. This will be a pain if we have a huge amount of data and various scenarios to test. I’ve referred a fix in the know issue section.
  3. We might see this error if we try to update real-time database using cloud functions.
  4. If we want to perform an operation on the realtime database from the cloud function then we need to set the local URL of the realtime database when we initialize the app. More here.
    Don’t forget to switch back to the production URL when you are deploying.
   admin.initializeApp({
        credential: admin.credential.applicationDefault(),
        databaseURL: 'http://localhost:9000?ns=projectId'
    });

5. Double-check your keys which we are using for firestore and realtime time. A typo can drain all your mental, physical, and spiritual energy.

Known Issues

Firebase emulator suite is new and far from perfect and sometimes it has bugs.

  1. Firestore UI bug: When we push data using the client we are not able to see the data in the firestore emulator page. But our data is pushed correctly from the client and the functionality is working as expected. I’ve already filed an issue here. Please upvote if you find the same.
  2. Persist data locally: Already discuss this in the above section. The Import/Export button on UI might help as we do for realtime using the JSON file.
  3. Authentication is not available locally.

Conclusion

I’ve developed an app using firebase before and believe me this feature saves a lot of time and energy.

Now tell your friend/colleague that we have a better way of testing the code using a firebase emulator instead of doing in production. It’s just beginning.

Looking forward to more features and bugs fixes. Thanks to Firebase Team 🙂

Thank you for taking the time to read this article. If you like this article, please like and share it with your friends/colleagues, and stay updated for this kind of post by subscribing to my blog.
In case of any question/doubts hit me on any social media platform @burhanrashid52.

Site Footer