cypress-firebase
Utilities and cli to help testing Firebase projects with Cypress
What?
If you are interested in what drove the need for this checkout the why section
Usage
Pre-Setup
Note: Skip cypress install if it already exists within your project
- Install Cypress and add it to your package file:
npm i --save-dev cypress
- Make sure you have a
cypress
folder containing cypress tests (or create one by calling cypress open
)
Setup
Note: These instructions assume your tests are in the cypress
folder (cypress' default). See the folders section below for more info about other supported folders.
-
Install using npm i cypress-firebase --save-dev
-
Make sure you have firebase-tools
installed (globally and within project). It is used to call to database when using cy.callRtdb
and cy.callFirestore
.
-
Add the following your custom commands file (cypress/support/commands.js
):
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
};
firebase.initializeApp(fbConfig);
attachCustomCommands({ Cypress, cy, firebase })
-
Setup plugin adding following your plugins file (cypress/plugins/index.js
):
const cypressFirebasePlugin = require('cypress-firebase').plugin
module.exports = (on, config) => {
return cypressFirebasePlugin(config)
}
The plugin sets baseUrl
and loads config from .firebaserc
-
If you plan to authenticate the user in your tests, continue to the next section which covers Auth
Auth
-
Log into your Firebase console for the first time.
-
Go to Auth tab of Firebase and create a user for testing purpose
-
Get the UID of created account. This will be the account which you use to login while running tests (we will call this UID TEST_UID
)
-
Add the following to your .gitignore
:
serviceAccount.json
cypress.env.json
-
Go to project setting on firebase console and generate new private key. See how to do here
-
Save the downloaded file as serviceAccount.json
in the root of your project (make sure that it is .gitignored)
-
Add the UID of the user you created earlier to your cypress environment file (cypress.env.json
) when running locally (make sure this is in you .gitignore
):
{
"TEST_UID": "<- uid of the user you want to test as ->"
}
In CI this will instead be loaded from the TEST_UID
environment variable
- Pass the UID when logging in:
cy.login(Cypress.env('TEST_UID'))
NOTE: If you are running tests within your CI provider you will want to set the SERVICE_ACCOUNT
environment variable as the service account object and the TEST_UID
environment variable as the UID of your test user
Running
- Start your local dev server (usually
npm start
) - for faster alternative checkout the test built version section - Open cypress test running by running
npm run test:open
in another terminal window
Docs
CLI Commands
createTestEnvFile {#createTestEnvFile}
Create test environment file (cypress.env.json
) which contains custom auth token generated using firebase-admin
SDK and serviceAccount.json
.
Requirements
A service account must be provided. This can be done by setting serviceAccount.json
in the root of the project (often used locally since service accounts should be in gitignore), or by setting the SERVICE_ACCOUNT
enviroment variable. For different environmets you can prefix with the environment name such as STAGE_SERVICE_ACCOUNT
.
Examples
cypress-firebase createTestEnvFile
Custom Cypress Commands
Table of Contents
cy.login
Login to Firebase auth using FIREBASE_AUTH_JWT
environment variable
which is generated using firebase-admin
authenticated with serviceAccount
during build:testConfig
phase.
Examples
cy.login()
cy.logout
Log out of Firebase instance
Examples
cy.logout()
cy.callRtdb
Call Real Time Database path with some specified action. Authentication is through FIREBASE_TOKEN
since firebase-tools is used (instead of firebaseExtra).
Parameters
action
String The action type to call with (set, push, update, remove)actionPath
String Path within RTDB that action should be appliedopts
object Options
opts.limitToFirst
number|boolean Limit to the first <num>
results. If true is passed than query is limited to last 1 item.opts.limitToLast
number|boolean Limit to the last <num>
results. If true is passed than query is limited to last 1 item.opts.orderByKey
boolean Order by key nameopts.orderByValue
boolean Order by primitive valueopts.orderByChild
string Select a child key by which to order resultsopts.equalTo
string Restrict results to <val>
(based on specified ordering)opts.startAt
string Start results at <val>
(based on specified ordering)opts.endAt
string End results at <val>
(based on specified ordering)opts.instance
string Use the database <instance>.firebaseio.com
(if omitted, use default database instance)opts.args
Array Command line args to be passed
Examples
Set data
const fakeProject = { some: 'data' }
cy.callRtdb('set', 'projects/ABC123', fakeProject)
Set Data With Meta
const fakeProject = { some: 'data' }
cy.callRtdb('set', 'projects/ABC123', fakeProject, { withMeta: true })
Get/Verify Data
cy.callRtdb('get', 'projects/ABC123')
.then((project) => {
cy.wrap(project)
.its('createdBy')
.should('equal', Cypress.env('TEST_UID'))
})
Other Args
const opts = { args: ['-d'] }
const fakeProject = { some: 'data' }
cy.callRtdb('update', 'project/test-project', fakeProject, opts)
cy.callFirestore
Call Firestore instance with some specified action. Authentication is through serviceAccount.json since it is at the base
level. If using delete, auth is through FIREBASE_TOKEN since firebase-tools is used (instead of firebaseExtra).
Parameters
action
String The action type to call with (set, push, update, remove)actionPath
String Path within RTDB that action should be appliedopts
Object Options
opts.args
Array Command line args to be passed
Examples
Basic
cy.callFirestore('set', 'project/test-project', 'fakeProject.json')
Recursive Delete
const opts = { recursive: true }
cy.callFirestore('delete', 'project/test-project', opts)
Other Args
const opts = { args: ['-r'] }
cy.callFirestore('delete', 'project/test-project', opts)
Full
describe('Test firestore', () => {
const TEST_UID = Cypress.env('TEST_UID');
const mockAge = 8;
beforeEach(() => {
cy.visit('http://localhost:4200');
});
it('read/write test', () => {
cy.log('Starting test');
cy.callFirestore('set', `testCollection/${TEST_UID}`, {
name: 'axa',
age: 8,
});
cy.callFirestore('get', `testCollection/${TEST_UID}`).then(r => {
cy.wrap(r[0])
.its('id')
.should('equal', TEST_UID);
cy.wrap(r[0])
.its('data.age')
.should('equal', mockAge);
});
cy.log('Ended test');
});
});
Recipes
Using Database Emulators
-
Install cross-env for cross system environment variable support: npm i --save-dev cross-env
-
Add the following to the scripts
section of your package.json
:
"emulators": "firebase emulators:start --only database,firestore",
"test": "cross-env CYPRESS_baseUrl=http://localhost:3000 cypress run",
"test:open": "cross-env CYPRESS_baseUrl=http://localhost:3000 cypress open",
"test:emulate": "cross-env FIREBASE_DATABASE_EMULATOR_HOST=\"localhost:$(cat firebase.json | jq .emulators.database.port)\" FIRESTORE_EMULATOR_HOST=\"localhost:$(cat firebase.json | jq .emulators.firestore.port)\" yarn test:open"
-
Add support in your application for connecting to the emulators:
const shouldUseEmulator = window.location.hostname === 'localhost'
if (shouldUseEmulator) {
console.log('Using RTDB emulator')
fbConfig.databaseURL = `http://localhost:9000?ns=${fbConfig.projectId}`
}
firebase.initializeApp(fbConfig)
if (shouldUseEmulator) {
console.log('Using Firestore emulator')
const firestoreSettings = {
host: 'localhost:8080',
ssl: false,
};
if (window.Cypress) {
firestoreSettings.experimentalForceLongPolling = true;
}
firebase.firestore().settings(firestoreSettings)
}
-
Make sure you also have matching init logic in cypress/support/commands.js
or cypress/support/index.js
:
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
}
const rtdbEmulatorHost = Cypress.env('FIREBASE_DATABASE_EMULATOR_HOST')
if (rtdbEmulatorHost) {
fbConfig.databaseURL = `http://${rtdbEmulatorHost}?ns=${fbConfig.projectId}`
}
firebase.initializeApp(fbConfig);
const firestoreEmulatorHost = Cypress.env('FIRESTORE_EMULATOR_HOST')
if (firestoreEmulatorHost) {
firebase.firestore().settings({
host: firestoreEmulatorHost,
ssl: false
})
}
attachCustomCommands({ Cypress, cy, firebase })
-
Start emulators: npm run emulators
-
In another terminal window, start the application: npm start
-
In another terminal window, open test runner with emulator settings: npm run test:emulate
NOTE: If you are using react-scripts or other environment management, you can use environment variables to pass settings into your app:
const { REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST, REACT_APP_FIRESTORE_EMULATOR_HOST } = process.env
if (REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST) {
console.log('Using RTDB emulator')
fbConfig.databaseURL = `http://${REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST}?ns=${fbConfig.projectId}`
}
firebase.initializeApp(fbConfig)
if (REACT_APP_FIRESTORE_EMULATOR_HOST) {
console.log('Using Firestore emulator')
const firestoreSettings = {
host: REACT_APP_FIRESTORE_EMULATOR_HOST,
ssl: false,
};
if (window.Cypress) {
firestoreSettings.experimentalForceLongPolling = true;
}
firebase.firestore().settings(firestoreSettings)
}
Generate JWT Before Run
-
Add the following to the scripts
section of your package.json
:
"build:testConfig": "cypress-firebase createTestEnvFile",
"test": "npm run build:testConfig && cypress run",
"test:open": "npm run build:testConfig && cypress open",
-
Add your config info to your environment variables (for CI) or cypress.env.json
when running locally (make sure this is in you .gitignore
)
{
"TEST_UID": "<- uid of the user you want to test as ->"
}
Testing Different Environments
Environment variables can be passed through --env
. envName
points to the firebase project within the projects section of .firebaserc
.
Test Built Version
Tests will run faster locally if you tests against the build version of your app instead of your dev version (with hot module reloading and other dev tools). You can do that by:
-
Adding the following npm script:
"start:dist": "npm run build && firebase serve --only hosting -p 3000",
-
Run npm run start:dist
to build your app and serve it with firebase
-
In another terminal window, run a test command such as npm run test:open
CI
- Run
firebase login:ci
to generate a CI token for firebase-tools
(this will give your cy.callRtdb
and cy.callFirestore
commands admin access to the DB) - Set
FIREBASE_TOKEN
within CI environment variables
Examples
Github Actions
Separate Install
name: Test Build
on: [pull_request]
jobs:
ui-tests:
name: UI Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v1
- name: Install Dependencies
uses: cypress-io/github-action@v1
with:
runTests: false
- name: Build Test Environment Config
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
TEST_UID: ${{ secrets.TEST_UID }}
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF: ${{ github.ref }}
run: |
$(npm bin)/cypress-firebase createTestEnvFile $TEST_ENV
- name: Cypress Run
uses: cypress-io/github-action@v1
with:
install: false
group: 'E2E Tests'
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF: ${{ github.ref }}
Using Start For Local
name: Test Hosted
on: [pull_request]
jobs:
ui-tests:
name: UI Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v1
- name: Install Dependencies
uses: cypress-io/github-action@v1
with:
runTests: false
- name: Build Test Environment Config
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
TEST_UID: ${{ secrets.TEST_UID }}
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF: ${{ github.ref }}
run: |
$(npm bin)/cypress-firebase createTestEnvFile $TEST_ENV
- name: Cypress Run
uses: cypress-io/github-action@v1
with:
install: false
group: 'E2E Tests'
start: npm start
wait-on: http://localhost:3000
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
GITHUB_REF: ${{ github.head_ref }}
Why?
It isn't currently possible to use Firebase's firebase-admin
SDK directly within Cypress due to dependencies not being able to be loaded into the Browser environment. Since firebase-admin
is nessesary to generate custom token needed to login to Firebase, the usage of it happens outside of Cypress (through cypress-firebase createTestEnvFile
) before booting up.
Instead of a cli tool, the plugin that is included could maybe use firebase-admin
(since cypress plugins is a node environment) - when investigating this, I found it frustrating to get the values back into the test. That said, always open to better ways of solving this, so please reach out with your ideas!
Projects Using It
fireadmin.io - A Firebase project management tool (here is the source)
Roadmap
- Fix issue where auth token goes bad after test suite has been open a long time