Starting the emulators in the development

Open the services/web/firebase/package.json file. We need to change the dev NPM script so that it starts the Firebase emulators.

"scripts": {
  "dev": "firebase emulators:start",
}

Let’s see this in action. In your terminal, navigate to services/web and start the development environment with npm run dev.

Note: If you do not have Java installed, you will be asked to do so, as this is a requirement from the Firebase emulators. You can find the latest version here.

In your browser, open http://localhost:4000 to look at the Firebase Emulator Suite. The Firestore tab displays your posts. Note, the database entries are deleted when you stop the dev server.

Gitpod users

In the Open Ports tab next to the terminal tabs, make sure you click “Make Public” for port 8080. This is so that the web application can connect to the local Firestore emulator.

Quick reminder, to access http://localhost:4000, open two terminal tabs. In the first tab, start Firefox by typing firefox and hit Enter. In the second, start the development server with npm run dev in the services/web directory.

Next, click on the “Open Ports” tab next to the terminal, locate 6080 and click “Open Browser” on the right.

In the new browser tab that opened, you can now use Firefox to access http://localhost:4000

Fix tests

One of our integration tests in services/web/cypress/integration/spec.js expects at least one blog post to be displayed on the /blog page. However, since we now use the Firebase Local Emulator Suite, our local Firestore data is deleted when we stop the emulator.

To fix this, we need to seed our database with a dummy blog post before we validate that the blog post is displayed on the page.

Install Firebase admin SDK

We start by installing the Firebase Admin SDK in services/web:

npm install -D firebase-admin

Create a cypress task

Next, we need to create a Cypress task that allows us to insert a blog post before we run our test. Let’s begin with the task definition itself.

Create a services/web/cypress/plugins/firestore.js file with the following content:

const admin = require("firebase-admin");

let firebaseApp;
let db;

const getFirebaseApp = (options) => {
  if (!firebaseApp) {
    firebaseApp = admin.initializeApp(options);
  }
  return firebaseApp;
};

const getFirestore = (firebaseAppOptions) => {
  if (!db) {
    db = getFirebaseApp(firebaseAppOptions).firestore();
  }
  return db;
};

module.exports = {
  addBlogPost({ firebaseAppOptions, slug, post }) {
    return new Promise(resolve => {
      getFirestore(firebaseAppOptions).collection("posts").doc(slug).set(post).then(() => {
        resolve(null);
      });
    });
  }
};

The important part is the addBlogPost function at the very bottom. Cypress requires tasks to return an object or null. Hence, we wrap the Firestore operation in a new promise and resolve it with null when the document is successfully inserted into the database.

The getFirestore() and getFirebaseApp() helpers allow us to cache the initialized objects to speed up tests.

Add environment variables

In order for the Firebase Admin SDK to connect to the emulator Firestore, we can use an environment variable that is picked up by the SDK. Open the services/web/package.json file and add the following environment variable to the beginning of the cy:run and cy:open NPM scripts:

FIRESTORE_EMULATOR_HOST='localhost:8080'

To make sure tests pass in our CI/CD pipeline, we also have to set that environment variable in the GitHub workflow. Open the .github/workflows/services-web.yml file and add the following to the test job’s env.

FIRESTORE_EMULATOR_HOST: "localhost:8080"

Make addBlogPost available in our test suite

We now need to register the addBlogPost task to make it available in our test suite. To do that, open services/web/cypress/plugins/index.js and update with the following two lines:

const firestoreTasks = require("./firestore");
...
module.exports = (on, config) => {
  ...
  on("task", firestoreTasks);
};

Great progress! The addBlogPost task is now registered. Open services/web/cypress/integration/spec.js.

Update tests

In the describe("Blog posts") section, remove the beforeEach() block and add the following before() function instead:

before(() => {
  cy.visit("/").then(contentWindow => {
    const firebaseAppOptions = contentWindow.firebase.app().options;
    cy.task("addBlogPost", {
      firebaseAppOptions,
      slug: "test-post",
      post: {
        content: "A test blog post",
        title: "Test post"
      }
    });
  });
  cy.visit("/blog");
});

Instead of loading /blog before each test (that’s what the beforeEach() did), we will instead:

  • Load the homepage cy.visit("/").
  • When it is loaded with .then(), we receive the browser’s window object, which contains our Firebase app options.
  • Call the new addBlogPost task and pass the necessary parameters to create a blog post.
  • Load the /blog page.

Lastly, change the “displays blog posts” test to “displays the test blog post” and replace the cy.get() line with the following:

cy.contains("[data-cy=blog-posts-list] a", "Test post");

This test now ensures the correct test post is displayed.

To make sure it all works, press RUN and follow the instructions to run the testsrunTest:

Get hands-on with 1200+ tech skills courses.