Angular Prototyping: Firebase Emulator with Cypress

Angular prototyping for faster development & testing with the Firebase Emulator and Cypress

Erxk
ITNEXT

--

About Me 👋

I currently work as a Software Engineer, alongside some of the best Angular developers in the industry, at the Fortune 100 company Cisco. I actively work on side projects in my free-time, which are powered by Angular & Firebase.

🔥 Source Code — Alternatively, each gist has a link to it’s corresponding file

Objectives 🗺️

  • 🦠 Utilize Firestore data for manual and e2e (cypress) testing
  • 🧬 Effectively create mock data in Firestore for prototyping & testing
  • 🧪 Edits and deletes don’t effect live data or count towards billing
  • 🔭 Conveniently stub Firebase requests
  • 🔒 Use emulator for Authentication in Cypress

Prototyping is when we utilize some sample or mock data to quickly test out an idea. Whenever I start a new project the first thing I do is setup architecture to effectively prototype. — Not to be confused with JavaScript Prototypes 🙂

This setup will enable us to use Authentication & Firestore data for all of our manual, and e2e testing. — This means that we won’t have to create json fixtures or cypress intercepts for e2e testing.

💵 Additionally, we won’t be charged for reads & writes to our live environment; as we’re using an emulator.

This is an intermediate article, if you’ve never setup a firebase project before I’d recommend this video Firebase Quickstart by FireShip

Installation 🔧

The following only works when leveraging Firebase. If you’re utilizing HttpClient see Angular Prototyping: Develop Faster with Prototyping.

While working with Angular and the Firebase Emulator. I found many pitfalls. To keep this article streamlined, I’ve added pitfalls to the end of the article. If you run into any problems, check there for help.

If you already have Firebase & the Emulator setup, skip ahead to Usage.

Requirements

  • Node.js version 8.0 or higher
  • Java version 1.8 or higher
  • Firebase CLI 8.14.0 or higher

You likely have Node.js installed if you’re doing Angular development. Make sure to install openjdk on your machine if you haven’t already.

npm install -g firebase-tools-- Angular
ng add @angular/fire
firebase init emulators
-- NX
npm install @angular/fire
ng add @nxtend/firebase
nx generate @nxtend/firebase:firebase-project --project NX-APP
nx run NX-APP:firebase --cmd=init
  • Install Firebase CLI (firebase-tools)
  • For both Angular & Nx projects, the following will install @angular/fire, add a firebase config, and setup the emulators. Make sure to select Firestore & Authentication when setting up Firebase & the Emulator.
  • For Angular projects, there’s generally only 1 project, so the setup is straightforward. For Nx projects, run init to tie an Angular project to a Firebase project.
init firebase
firebase emulator setup

Configuration ⚙️

I use the flag environment.production to determine whether or not to use the emulator. In my other prototyping article I used a custom flag / environment for the In-Memory-API.

To keep this article short and to the point, I’ve created a separate companion article in case you want to know how to setup a custom environment.

Important: These examples use @angular/fire v7.

Setup the App

In your app.module.ts add the configuration. It’s kind of long, but I wanted to make sure the imports are clear, and you can remove what you don’t need.

SOURCE
  • The only necessary provider is provideFirebaseApp, you can remove what you don’t need or change persistence options, ports, host, etc…
  • environment.firebaseConfig is your standard firebase config, just saved to an environment file.
  • experimentalForceLongPolling is for usage in Cypress, thus we only want it on in dev.

💡 Configures a firebase app with the firestore & auth emulator

Usage — Prototyping & Testing 🧪

Below is a basic component to verify that our emulator is working. It pulls from a collection called tickets and loops over the results.

SOURCE
firebase emulators:start
  • Navigate to localhost:4202 to see the emulator UI. You should be able to navigate to and see an empty Firestore instance. You can add mock data here if your app is not setup yet.
  • Changes we make in our app should be reflected in the emulator and visible in the emulator UI
  • A nearly blank screen with some JSON isn’t very exciting. Below is a production example of the Firestore Emulator in action.
Emulator Example

The example above works using purely mock data from the Emulator

  • 💡 We can quickly generate and modify mock data for prototyping ideas, proof of concepts, new features, testing bugs
  • 💵 We are not being charged for reads and writes to our live environment
  • 🔥 Firebase is conveniently mocked, making it easier to test with realistic data, more on this later.

To save any changes (Replace FOLDER_NAME) [Example Result]

-- Save Mock Data
firebase emulators:export FOLDER_NAME
-- Start Emulator with Mock Data
firebase emulators:start --import=FOLDER_NAME

Now we can easily setup / tear-down an environment with that dataset as a baseline. Lets look at how we can utilize that data in our e2e tests.

Caveat: Using in Jest (Unit Tests) ❌

I created a proof of concept doing this but have opted to not add it to this article. The number of complexities and drawbacks is too high to make it worth doing (imo).

  • We have to use node as the jest environment.
  • We have to add our firebase config to each test
  • The Firebase Emulator doesn’t reset each test like the In-Memory API.
  • Outside of unit testing firebase rules, there’s very little documentation / support for this kind of thing

Using in Cypress (E2E Tests*) 🌳

Prior to this strategy I would keep datasets on development environments that I knew couldn’t be changed or my tests might break 😱. This was a very fragile approach. Using the emulator with Cypress improves this process significantly

-- NX
nx run APP-e2e:e2e --watch
-- Angular
ng serve
cypress open
Cypress Test
  • I use this strategy for all of my E2E testing.
  • It uses 0 json fixtures and 0 cypress intercepts.
  • Caveat, remember that the emulator only clears data on shutdown. On a CLI this is fine because we always run from a clean state. But if you’re working with cypress locally you may have to clear your data or write tests that go back to their original state.

Setting up Auth in Cypress

Earlier, we mentioned it was important to use @angular/fire imports so that the providers would work. In this case, since we are outside the context of Angular (in Cypress code), we need to use @firebase imports. — In our commands.ts:

SOURCE
  • These are commands that we can reuse and call in any of our Cypress tests
  • When called, the authentication token is persisted to the browser, the original auth provider we setup in app.module then picks up the token.
  • i.e. All of our Angular auth logic will still work. Even though in the Cypress command, auth was setup with firebase/auth, and not @angular/fire/auth

Before running the test below, make sure we have at least one user. If we don’t have a user, we can create one in the emulator ui at localhost:4202

Yes, my users are named Mountain Chicken and Peach Otter
SOURCE
  • To run our project authenticated, we call cy.init to setup the auth providers in Cypress. We then call cy.login with the email/password of the emulator user
  • Don’t forget to run firebase emulators:export FOLDER_NAME to save your user.

⚡ TLDR — This setup allows you to login with a firebase emulator user while running Cypress tests

Conclusion

Below are pitfalls I ran into. A huge drive for me to write this article in the first place was all of the pitfalls. Hopefully this article can help you get through some of these issues faster.

*In this context e2e is used broadly. Since we’re not hitting a live backend, I would call this “functional” testing.

☁️ Flotes — Try the demo, no login required. Or sign up for free. Flotes is how I take notes and learn efficiently, even when I’m busy.

Flotes

Pitfalls 🪤

firebase emulators:start hangs

firebase emulators:start hangs for a minute, then shuts down.

To resolve this I had to make sure I had the a compatible node.js version and firebase version. I’m currently using:

firebase: 10.0.1
npm: 8.1.2
node: 16.13.1
openjdk: 11.0.15

FirebaseError: PERMISSION_DENIED: No matching allow statements

This stems from your Firebase rules. When I initialized my project, I didn’t realize that my server side rules were copied over to my local project, which I did not want. Docs

Angular/Fire v7

Angular Fire released a new Modular SDK. This means you can use Angular Fire just like the vanilla Firebase SDK with some RxJS convenience operators on top of it.

NullInjectorError: No provider for InjectionToken angularfire2.app.options

AngularFire v7 provides the new modular SDK and a compatibility layer alternative to use AngularFire how it has been in the past. The two do not share providers. i.e. You may have setup/provided your firebase config via something like provideFirestore and then tried to use private afs: AngularFirestore. In this case you would need to either import AngularFirestoreModule or replace AngularFirestore with it’s v7 equivalent.

Module not found: Error: Can't resolve '@angular/core'

You may have imported @angular/fire/FOLDER outside of the context of your Angular app. You’ll have to use firebase/FOLDER imports outside of Angular (For example, in Cypress)

collectionChanges Doesn't Emit when Empty

Methods such as collectionChanges which could be extremely beneficial and are used in downstream projects, are (imo) essentially unusable because of issues like this and this (downstream)

You can alternatively use collectionData instead to get all emissions.

When using collection(): Expected 0 type arguments but got 1

As an alternative, you can use collection(firestore, ‘tickets’) as CollectionReference<Tickets>;. — See issue

Cypress won't load firestore data

You need to enable experimentalForceLongPolling.

provideFirestore(() => {
const firestore = initializeFirestore(getApp(), {
experimentalForceLongPolling: environment.production ? false : true,
}
);
return firestore;
}),

--

--