DEV Community

Cover image for Unit testing Firebase Firestore & Cloud Functions
Kyle Welsby
Kyle Welsby

Posted on • Updated on

Unit testing Firebase Firestore & Cloud Functions

A personal project of mine has me open the pandora's box of all fun and new technology. I usually am not able to use in my daytime job — Firebase Firestore and Cloud Functions (Lambdas for you AWS folk). 🤖

I challenged myself to write a function that takes a payload of data and creates a record in Firebase. Along with this challenge, I wanted to wrap my functionality with unit-tests as a new stretch goal.

The official Firebase Cloud Functions documentation is easy to read and understand for very basic use-cases. I wanted to go the extra mile beyond the primary examples. 😄

Code

Here I have a simple function that listens to a Firestore document created event. It will invoke the Cloud Function to take the data, check if it exists and if not, create an associating record.

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();
let db = admin.firestore();

exports.onEpisodeTrackCreated = functions.firestore.document('episodes/{episodeId}/tracks/{trackIndex}')
  .onCreate((snap, context) => {
    const data = snap.data()

    if (!data.name) throw new Error('Missing `name` parameter')

    const name = data.name.trim()
    let tracksRef = db.collection('tracks')

    return tracksRef.where('name', '==', name).get()
      .then(snapshot => {
        if (snapshot.empty) {
          return tracksRef.add({
            name: name
          })
        }
        let doc
        snapshot.forEach(snapDoc => {
          doc = snapDoc
        })
        return doc
      })
        .then((doc) => {
          snap.ref.set({
            trackId: doc.id
          }, { merge: true })
          return doc
        })
  })

Test Setup

Install firebase-functions-test and Jest; a popular "batteries included" testing framework.

npm install --save-dev firebase-functions-test jest

We'll need to create a test folder where we will store the unit-tests for our functions.

Next, I updated the package.json with the test script to call.

"scripts": {
"test": "jest test/"
}

Firebase Cloud Functions can run in Online and Offline modes. Online mode means it will interact with your Firebase account, create/destroy data. Offline mode will result in us stubbing our calls, and this is the preferred option in my opinion for this writing.

Initialize the SDK in offline mode by not defining any configuration options.

const test = require('firebase-functions-test')();

Let us continue with writing our unit test that invokes the function and should successfully resolve with the async/await otherwise it will throw an error.

const test = require('firebase-functions-test')();
const functions = require('../index.js');

describe('onEpisodeTrackCreated', () => {
  it('successfully invokes function', async () => {
    const wrapped = test.wrap(functions.onEpisodeTrackCreated);
    const data = { name: 'hello - world', broadcastAt: new Date() }
    await wrapped({
      data: () => ({
        name: 'hello - world'
      }),
      ref:{
        set: jest.fn()
      }
    })
  })
})

What happens when we run the test now? 🤔


 FAIL  tests/index.test.js
  onEpisodeTrackCreated
    ✕ successfully invokes function (832ms)

  ● onEpisodeTrackCreated › successfully invokes function

    Could not load the default credentials. Browse to https://cloud.google.com/docs/authenti
cation/getting-started for more information.

      at GoogleAuth.getApplicationDefaultAsync (node_modules/google-auth-library/build/src/auth/googleauth.js:161:19)
      at GoogleAuth.getClient (node_modules/google-auth-library/build/src/auth/googleauth.js:503:17)
      at GrpcClient._getCredentials (node_modules/google-gax/src/grpc.ts:150:20)
      at GrpcClient.createStub (node_modules/google-gax/src/grpc.ts:295:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.91s

😢 this is not good.

Thinking about this error a bit more, we do have quite a bit going on in our code. This error is telling us something about the credentials. Perhaps it is to do with the initializeApp on the firebase-admin? 🤔

We'll mock that and see what happens next.

jest.mock('firebase-admin', () => ({
  initializeApp: jest.fn()
}))

And the result...

 FAIL  tests/index.test.js
  ● Test suite failed to run

    TypeError: admin.firestore is not a function

      3 | 
      4 | admin.initializeApp();
    > 5 | let db = admin.firestore();
        |                ^
      6 | 
      7 | exports.onEpisodeTrackCreated = functions.firestore.document('episodes/{episodeId}/tracks/{trackIndex}')
      8 |   .onCreate((snap, context) => {

      at Object.firestore (index.js:5:16)
      at Object.require (tests/index.test.js:23:19)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.757s

Brilliant, this is a better position to be in. Because we're calling out to firestore but we've completely mocked the implementation this is as expected.

Now to complete the mocking for this test. 😅

const mockQueryResponse = jest.fn()
mockQueryResponse.mockResolvedValue([
  {
    id: 1
  }
])

jest.mock('firebase-admin', () => ({
  initializeApp: jest.fn(),
  firestore: () => ({
   collection: jest.fn(path => ({
     where: jest.fn(queryString => ({
       get: mockQueryResponse
     }))
   })) 
  })
}))

And the final run. 😬

 PASS  tests/index.test.js
  onEpisodeTrackCreated
    ✓ successfully invokes function (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.026s

Brilliant. 🙌

I really hope this solution helps you with testing your next project.

Sources

Of course, this result did not come about organically, it took a great deal of searching through the internet for relevant solutions through the coding phase.

Oldest comments (5)

Collapse
 
beatyt profile image
beatyt

This post was very helpful for me! I've spent 2 days trying to figure out how to do offline unit testing for firebase, and this finally got me there lol.

Collapse
 
kylewelsby profile image
Kyle Welsby

I am glad this post helped you. It was quite a journey through the internets and writings like these help future you and me.

Collapse
 
rphlmr profile image
Raphaël Moreau

Thank you !
I was looking for a guide like this :)

Collapse
 
kylewelsby profile image
Kyle Welsby

I am glad you found this useful.

Collapse
 
lucasgeitner profile image
Lucas Geitner • Edited

You can also use firestore simulator to have only local data and you don't need the mocking ;)

if (process.env.NODE_ENV !== 'test') {
admin = admin.initializeApp()
firestore = admin.firestore;
db = admin.firestore();
auth = admin.auth;
}

if (process.env.NODE_ENV === 'test') {
const firebasetest = require('@firebase/testing');
admin = firebasetest.initializeAdminApp({ projectId });
db = admin.firestore();
auth = admin.auth;
firestore = admin.firestore;
}

module.exports = { db, admin, firestore, auth };