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.