Giter VIP home page Giter VIP logo

cypress-firebase's Introduction

cypress-firebase

NPM version NPM downloads Build Status Coverage License Code Style

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

  1. If you do not already have it installed, install Cypress and add it to your package file: npm i --save-dev cypress or yarn add -D cypress
  2. 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.

  1. Install cypress-firebase and firebase-admin both: npm i --save-dev cypress-firebase firebase-admin or yarn add -D cypress-firebase firebase-admin --save-dev

  2. Go to project setting on firebase console and generate new private key. See how to do so in the Google Docs.

  3. Add serviceAccount.json to your .gitignore (THIS IS VERY IMPORTANT TO KEEPING YOUR INFORMATION SECURE!)

  4. Save the downloaded file as serviceAccount.json in the root of your project (make sure that it is .gitignored) - needed for firebase-admin to have read/write access to your DB from within your tests

  5. 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 = {
      // Your config from Firebase Console
    };
    
    firebase.initializeApp(fbConfig);
    
    attachCustomCommands({ Cypress, cy, firebase });
  6. Make sure that you load the custom commands file in an cypress/support/index.js like so:

    import "./commands";

    NOTE: This is a pattern which is setup by default by Cypress, so this file may already exist

  7. Setup plugin adding following your plugins file (cypress/plugins/index.js):

    const admin = require("firebase-admin");
    const cypressFirebasePlugin = require("cypress-firebase").plugin;
    
    module.exports = (on, config) => {
      const extendedConfig = cypressFirebasePlugin(on, config, admin)
    
      // Add other plugins/tasks such as code coverage here
    
      return extendedConfig
    };
  8. To confirm things are working, create a new test file (cypress/integration/examples/test_hello_world.js) adding a test that uses the cypress-firebase custom command (cy.callFirestore):

    describe("Some Test", () => {
      it("Adds document to test_hello_world collection of Firestore", () => {
        cy.callFirestore("add", "test_hello_world", { some: "value" });
      });
    });
  9. From the root of your project, start Cypress with the command $(npm bin)/cypress open. In the Cypress window, click your new test (test_hello_world.js) to run it.

  10. Look in your Firestore instance and see the test_hello_world collection to confirm that a document was added.

  11. Pat yourself on the back, you are all setup to access Firebase/Firestore from within your tests!

Auth

  1. Go to Authentication page of the Firebase Console and select an existing user to use as the testing account or create a new user. This will be the account which you use to login while running tests.

  2. Get the UID of the account you have selected, we will call this UID TEST_UID

  3. Set the UID of the user you created earlier to the Cypress environment. You can do this using a number of methods:

    • Adding CYPRESS_TEST_UID to a .env file which is gitignored

    • Adding TEST_UID to cypress.env.json (make sure you place this within your .gitignore)

    • Adding as part of your npm script to run tests with a tool such as cross-env here:

      "test": "cross-env CYPRESS_TEST_UID=your-uid cypress open"
  4. Call cy.login() with the before or beforeEach sections of your tests

Running

  1. Start your local dev server (usually npm start) - for faster alternative checkout the test built version section
  2. Open cypress test running by running npm run test:open in another terminal window

Considerations For CI

  1. Add the following environment variables in your CI:

    • CYPRESS_TEST_UID - UID of your test user
    • SERVICE_ACCOUNT - service account object

Docs

Custom Cypress Commands

Table of Contents

cy.login

Login to Firebase using custom auth token

Examples

Loading TEST_UID automatically from Cypress env:

cy.login();

Passing a UID

const uid = "123SomeUid";
cy.login(uid);

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 applied
  • options object Options
    • options.limitToFirst number|boolean Limit to the first <num> results. If true is passed than query is limited to last 1 item.
    • options.limitToLast number|boolean Limit to the last <num> results. If true is passed than query is limited to last 1 item.
    • options.orderByKey boolean Order by key name
    • options.orderByValue boolean Order by primitive value
    • options.orderByChild string Select a child key by which to order results
    • options.equalTo string Restrict results to <val> (based on specified ordering)
    • options.startAt string Start results at <val> (based on specified ordering)
    • options.endAt string End results at <val> (based on specified ordering)
Examples

Set data

const fakeProject = { some: "data" };
cy.callRtdb("set", "projects/ABC123", fakeProject);

Set Data With Meta

const fakeProject = { some: "data" };
// Adds createdAt and createdBy (current user's uid) on data
cy.callRtdb("set", "projects/ABC123", fakeProject, { withMeta: true });

Get/Verify Data

cy.callRtdb("get", "projects/ABC123").then((project) => {
  // Confirm new data has users uid
  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, delete)
  • actionPath String Path within RTDB that action should be applied
  • dataOrOptions String Data for write actions or options for get action
  • options Object Options
    • options.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("/");
  });

  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.log("get returned: ", r);
      cy.wrap(r).its("data.age").should("equal", mockAge);
    });
    cy.log("Ended test");
  });
});

Recipes

Using Database Emulators

  1. Install cross-env for cross system environment variable support: npm i --save-dev cross-env

  2. Add the following to the scripts section of your package.json:

    "emulators": "firebase emulators:start --only database,firestore",
    "test": "cypress run",
    "test:open": "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"
  3. If not already set by firebase init, add emulator ports to firebase.json:

    "emulators": {
      "database": {
        "port": 9000
      },
      "firestore": {
        "port": 8080
      }
    }
  4. Modify your application code to connect to the emulators (where your code calls firebase.initializeApp(...)), updating the localhost ports as appropriate from the emulators values in the previous step:

    const shouldUseEmulator = window.location.hostname === "localhost"; // or other logic to determine when to use
    // Emulate RTDB
    if (shouldUseEmulator) {
      fbConfig.databaseURL = `http://localhost:9000?ns=${fbConfig.projectId}`;
      console.debug(`Using RTDB emulator: ${fbConfig.databaseURL}`);
    }
    
    // Initialize Firebase instance
    firebase.initializeApp(fbConfig);
    
    const firestoreSettings = {};
    // Pass long polling setting to Firestore when running in Cypress
    if (window.Cypress) {
      // Needed for Firestore support in Cypress (see https://github.com/cypress-io/cypress/issues/6350)
      firestoreSettings.experimentalForceLongPolling = true;
    }
    
    // Emulate Firestore
    if (shouldUseEmulator) {
      firestoreSettings.host = "localhost:8080";
      firestoreSettings.ssl = false;
      console.debug(`Using Firestore emulator: ${firestoreSettings.host}`);
    
      firebase.firestore().settings(firestoreSettings);
    }
  5. Make sure you also have 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 = {
      // Your Firebase Config
    };
    
    // Emulate RTDB if Env variable is passed
    const rtdbEmulatorHost = Cypress.env("FIREBASE_DATABASE_EMULATOR_HOST");
    if (rtdbEmulatorHost) {
      fbConfig.databaseURL = `http://${rtdbEmulatorHost}?ns=${fbConfig.projectId}`;
    }
    
    firebase.initializeApp(fbConfig);
    
    // Emulate Firestore if Env variable is passed
    const firestoreEmulatorHost = Cypress.env("FIRESTORE_EMULATOR_HOST");
    if (firestoreEmulatorHost) {
      firebase.firestore().settings({
        host: firestoreEmulatorHost,
        ssl: false,
      });
    }
    
    attachCustomCommands({ Cypress, cy, firebase });
  6. Start emulators: npm run emulators

  7. In another terminal window, start the application: npm start

  8. In another terminal window, open test runner with emulator settings: npm run test:emulate

NOTE: If you are using react-scripts (from create-react-app) 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;

// Emulate RTDB if REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST exists in environment
if (REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST) {
  console.debug(`Using RTDB emulator: ${fbConfig.databaseURL}`);
  fbConfig.databaseURL = `http://${REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST}?ns=${fbConfig.projectId}`;
}

// Initialize Firebase instance
firebase.initializeApp(fbConfig);

const firestoreSettings = {};

if (window.Cypress) {
  // Needed for Firestore support in Cypress (see https://github.com/cypress-io/cypress/issues/6350)
  firestoreSettings.experimentalForceLongPolling = true;
}

// Emulate RTDB if REACT_APP_FIRESTORE_EMULATOR_HOST exists in environment
if (REACT_APP_FIRESTORE_EMULATOR_HOST) {
  firestoreSettings.host = REACT_APP_FIRESTORE_EMULATOR_HOST;
  firestoreSettings.ssl = false;

  console.debug(`Using Firestore emulator: ${firestoreSettings.host}`);

  firebase.firestore().settings(firestoreSettings);
}

Test Built Version

It is often required to run tests against the built version of your app instead of your dev version (with hot module reloading and other dev tools). You can do that by running a build script before spinning up the:

  1. Adding the following npm script:
    "start:dist": "npm run build && firebase emulators:start --only hosting",
  2. Add the emulator port to firebase.json:
    "emulators": {
      "hosting": {
        "port": 3000
      }
    }
  3. Run npm run start:dist to build your app and serve it with firebase
  4. In another terminal window, run a test command such as npm run test:open

NOTE: You can also use firebase serve:

"start:dist": "npm run build && firebase serve --only hosting -p 3000",

CI

  1. 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)
  2. Set FIREBASE_TOKEN within CI environment variables

Changing Custom Command Names

Pass commandNames in the options object to attachCustomCommands:

const options = {
  // Key is current command name, value is new command name
  commandNames: {
    login: 'newNameForLogin',
    logout: 'newNameForLogout',
    callRtdb: 'newNameForCallRtdb',
    callFirestore: 'newNameForCallFirestore',
    getAuthUser: 'newNameForGetAuthUser',
  }
}
attachCustomCommands({ Cypress, cy, firebase }, options);

For more information about this feature, please see the original feature request.

Webpack File Preprocessing

If you are using a file preprocessor which is building for the browser environment, such as Webpack, you will need to make sure usage of fs is handled since it is used within the cypress-firebase plugin. To do this with webpack, add the following to your config:

node: {
  fs: "empty";
}

See #120 for more info

Examples

Github Actions

Separate Install

name: Test Build

on: [pull_request]

jobs:
  ui-tests:
    name: UI Tests
    runs-on: ubuntu-16.04
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v2

      # Cypress action manages installing/caching npm dependencies and Cypress binary.
      - name: Cypress Run
        uses: cypress-io/github-action@v1
        with:
          group: "E2E Tests"
        env:
          # pass the Dashboard record key as an environment variable
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
          # UID of User to login as during tests
          CYPRESS_TEST_UID: ${{ secrets.TEST_UID }}
          # Service Account (used for creating custom auth tokens)
          SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
          # Branch settings
          GITHUB_HEAD_REF: ${{ github.head_ref }}
          GITHUB_REF: ${{ github.ref }}

Using Start For Local

name: Test

on: [pull_request]

jobs:
  ui-tests:
    name: UI Tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v2

      # Cypress action manages installing/caching npm dependencies and Cypress binary
      - name: Cypress Run
        uses: cypress-io/github-action@v1
        runs-on: ubuntu-16.04
        with:
          group: "E2E Tests"
          start: npm start
          wait-on: http://localhost:3000
        env:
          # pass the Dashboard record key as an environment variable
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
          # UID of User to login as during tests
          CYPRESS_TEST_UID: ${{ secrets.TEST_UID }}
          # Service Account (used for creating custom auth tokens)
          SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
          # Branch settings
          GITHUB_HEAD_REF: ${{ github.head_ref }}
          GITHUB_REF: ${{ github.ref }}

Why?

When testing, tests should have admin read/write access to the database for seeding/verifying data. It isn't currently possible to use Firebase's firebase-admin SDK directly within Cypress tests due to dependencies not being able to be loaded into the Browser environment. Since the admin SDK is necessary to generate custom tokens and interact with Real Time Database and Firestore with admin privileges, this library provides convenience methods (cy.callRtdb, cy.callFirestore, cy.login, etc...) which call custom tasks which have access to the node environment.

Projects Using It

cypress-firebase's People

Contributors

adg29 avatar arku avatar axacheng avatar brianvanburken avatar dependabot[bot] avatar dhair-seva avatar gregfenton avatar julioprotzek avatar lautapercuspain avatar mouradsm avatar prescottprue avatar seki2020 avatar shiva avatar

Stargazers

 avatar

Watchers

 avatar

cypress-firebase's Issues

cy.getAuthUser

Trying to use the cy.getAuthUser custom command but observed a firebase error on invocation. I can make do by sending uid taskSetting as part of the invocation at https://github.com/prescottprue/cypress-firebase/blame/master/src/attachCustomCommands.ts#L437 and a bit of refactoring to support the getAuthUser invocation with a signature similar to createCustomToken at https://github.com/prescottprue/cypress-firebase/blame/master/src/plugin.ts#L28

It was unclear to me how to differentiate between commands and tasks but this post helped clarify that. https://stackoverflow.com/a/58680884/367495

feature request: cy.getIdToken command

Id like to be able to get and idToken in a beforeEach or a custom command. The idToken will be used to seed my database via an API.

Based on the implementation of cy.login, I'll be using the firebase instance in context to first check if there is an authorized user and then return a getIdToken promise from a firebase instance.

For example:

                // Resolve with current user if they already exist
                if (
                    firebase.auth().currentUser &&
                    uid === firebase.auth().currentUser.uid
                ) {
                    return firebase.auth().currentUser.getIdToken()
                }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.