Testing React + Firebase Apps With Cypress

Prescott Prue
6 min readDec 1, 2018

Cypress is a great tool for testing the UI of applications, but it was built assuming that your app follows the standard model of data being loaded from a REST API. This assumption means that there isn’t much in the way of examples for systems where database interactions happen socketed clients in systems like Firebase RTDB and Firestore.

Simple page loading examples were not hard to get working, but when starting to try to access protected data/pages a number of questions come up:

How would auth even work for a test user? Going through the full login flow before every test feels wrong, but how else do I access protected data? How do I go about verifying that data was correctly written to the database? What about tests that require data to already exist?

All very valid questions that I actually also asked myself. For me, answering these questions took a considerable amount of time, and at first lead to some patterns that didn’t work out so well (ask me to go out for drinks/chat for the full story). After a new approach and testing on a number of different Firebase applications of different sizes, a solid pattern started to emerge.

After I noted the patterns/utilities being repeated across multiple of my personal applications, including fireadmin.io, and at Reside, it felt like it may be useful to wrap up into a library for others to use, so cypress-firebase was born.

Since cypress-firebase is still young, this article is going to start with simply covering how to add the tooling to an existing Firebase app then move onto examples of how to write some basic tests that require auth and interact with the database. There are bound to be more docs and posts to come as the toolset matures.

What does it do? Why?

A key part of making UI tests useful is getting each test to cover only a small section of your application and have everything it needs to run in isolation (alone). In order to do this effectively, some of your tests will most likely require adding data to your database (seeding) or verifying that your app updates the database with the correct values (verifying app writes).

Custom Auth Token

Going to the login page and waiting for full login within every test not only adds tons of time to each test, it can also be unreliable (especially since external login UIs often change) — instead we can authenticate with a custom auth token. Firebase’s firebase-admin JS SDK allows you to generate custom auth tokens for your users based on the user’s UID and also allows for an extra payload.

Due to some dependencies not being able to be loaded into the Browser environment (which cypress runs within), firebase-admin cannot be called from directly within Cypress. To solve this issue, we can move the usage of firebase-admin to a command line utility that is called through cy.exec — this is where cypress-firebase comes in with the createTestEnvFile command. Calling cypress-firebase createTestEnvFile before booting up will create the custom token using a service account and add it to the cypress environment (cypress.env.json ).

DB Interactions Use Admin Auth

Adding data for your tests (seeding) and checking the application correctly updates the database (verifying) should be done with admin access since you will be reading/updating/removing data that may not be owned by the user account which is logged into the application while testing. Doing this is also helpful when it comes time to test using multiple different user account types.

In order to use admin auth for DB interactions, the custom commands cy.callRtdb and cy.callFirestore have been included with cypress-firebase. Internally they call firebase-tools which has privileges thanks to already being logged in on your machine or through FIREBASE_TOKEN in a CI environment. Since firebase-tools does not include all of the necessary actions to do full verifying/seeding, especially when it comes to Firestore, some actions call firebase-tools-extra instead (which uses a service account for authorization).

Now that we have covered how things work lets try adding cypress-firebase to an existing project.

Getting Started

Install

npm i cypress-firebase --save-dev

Add Local Config And Service Account

  1. Log into your app for the first time to generate an Auth object
  2. Go to the Auth tab of Firebase and get your UID
  3. Generate a service account -> save it as serviceAccount.json
  4. Add your config info to cypress/config.json
{
“TEST_UID”: “<- uid of the user you want to test as ->”,
“FIREBASE_PROJECT_ID”: “<- projectId of your project ->”
}

Pass Firebase Instance to App

In order to use the same Firebase instance as the one that we authenticate during our tests, we will use the instance from the window if it is set. To make this happen, add a store.js that looks like so:

Add NPM Scripts

Add the following npm scripts to your package.json

"build:testConfig": "cypress-firebase createTestEnvFile",
"test": "npm run build:testConfig && cypress run",
"test:open": "npm run build:testConfig && cypress open",
"test:stage": "npm run test -- --env envName=stage",
"test:open:stage": "npm run test:open -- --env envName=stage",

Add Custom Commands

Cypress supports adding your own custom command API through adding custom commands on the Cypress object. Add the following to cypress/support/commands.js:

For more details on how custom commands work, visit the Cypress Docs on custom commands.

Add Plugin To Load Firebase Config

To keep the npm scripts a little more clear we can set the baseUrl based on an environment flag that we pass in the command with a simple cypress plugin. The settings are loaded from your .firebaserc file.

Add the following to your cypress/plugins/index.js file:

Element Selectors

Any React Components or HTML elements you plan to interact with from within your tests require some way of being identified. I have found that using the data-test attribute, as suggested in the best practices section of the Cypress docs, is a better way of selecting then class names or element names. That way, regardless of how the styling of the application changes your tests can still select the correct elements.

To do this, just apply the attribute to the element:

<button data-test="submit-btn">Submit</button>

Then it can be selected easily within your tests:

cy.get('[data-test=submit-btn]').click()

If you get tired of repeating yourself within selectors, you can make a simple function to extract away the attribute piece:

Simple selector utility

Then your get code is even more simple:

cy.get(createSelector('submit-btn')).click()

Writing Tests

Page Requiring Auth

It is common for applications to have some routes or features that require auth in order to work. To handle this we will login as a test user using cy.login then attempt to use the protected feature (adding a project in this case). Below are two sections of tests for the Projects Page — The first is a test to confirm that visiting the projects page when not logged in causes the user to be redirect back to the home page (/). The second section has a test to verify that navigating to the /projects when logged in does not cause a redirect (login and navigation in the before).

Verify App Writes Data To Database

In order to verify that your application is updating the database correctly, we will first interact with the application then use cy.callRtdb with the get action to load data from the database. Once the data is loaded, we can confirm that it has the correct format and values.

First we will need to add a data-test attribute to displayName field and the save button (following the pattern mentioned in the Elements Selectors section above). Then we can write a test that types in that field like so:

Seeding The Database

Placing data within the database before running a test, or “seeding”, is a key piece of having tests stand free from each other. In the next example we will be creating a fake project with the current user’s info, placing it within the database, then navigating to a page that loads the project data.

Wrapping Up

Using these patterns, you can write more complete tests that fit your application’s features. Below are details for a starting place for a new project and a full example of an application using this testing method.

Full Project Example

If you are looking for an application that is using this setup to test real application features, checkout the ui test code of fireadmin.io.

Starting A New Project

If you are looking to start a project with these tools, checkout generator-react-firebase to start a React + Firebase project with the option for tests for React Components (Jest), UI Tests (Cypress), and Cloud Functions Tests (Mocha/Chai).

Interested In Your Input

This is just one approach, but it is by no means the only way. Please feel free to reach out if you have questions or input about how things could be done differently.

--

--

Prescott Prue

Author of react-redux-firebase and generator-react-firebase. Mechanical/Aerospace Engineer turned full-stack JS engineer