Unit testing Firebase Firestore & Cloud Functions

Kyle Welsby
Oct 1 · 4 min read

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.

JavaScript in Plain English

Learn the web's most important programming language.

Kyle Welsby

Written by

Code Wrangler, Travel & Food Photographer, and Maker of http://soulectiontracklists.com . From Great Britain 🇬🇧

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade