Unit testing Firebase Firestore & Cloud Functions

kylewelsby

Kyle Welsby

Posted on September 30, 2019

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.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
kylewelsby
Kyle Welsby

Posted on September 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related