Your little Firebase project is getting bigger every day? Never underestimate the need to establish a solid and firm integration tests from the get go.
Once you start to utilize various features of Firebase, from Hosting, Functions, and Firestore, it is imperative to incorporate practical local testing as soon as possible. Not only it will save you from some potential nightmares down the road, it can also facilitate faster iterations and quick(er) turn-around time during refactoring and feature implementation. Here is a few random suggestions to get you started. To follow along, you can also check the git repository containing the sample code at github.com/ariya/hello-firebase-experiment.
First thing that you always need to do is to implement a health check functionality. The name could be as simple as ping
. Hence, inside your main Firebase Functions, there should be a block of code that looks like:
exports.ping = functions.https.onRequest((request, response) => {
response.send('OK');
});
Now if you want to be fancy, it does not hurt to show the timestamp (Unix epoch) which can be valuable to know that this is not a cached or outdated HTTP response. If you wish, feel free to extend it with useful tidbits (but be careful not to reveal sensitive information).
exports.ping = functions.https.onRequest((request, response) => {
response.send(`OK ${Date.now()}`);
});
In your test code (shown here with Axios to perform an HTTP request, but the concept applies to any library), do a quick sanity check that this /ping
is working. This is an important step towards a reliable local testing.
it('should have a working ping function', async function () {
const res = await axios.get('http://localhost:5000/ping');
const status = res.data.substr(0, 2);
const timestamp = res.data.substr(3);
expect(status).toEqual('OK');
expect(timestamp).toMatch(/[0-9]+/);
});
Now, the test might fail miserably. If that is the case, you do not have the proper setup yet to use and run Firebase emulators. Using npm, make sure to install all the following packages:
firebase-tools
firebase-functions
firebase-functions-test
firebase-admin
@google-cloud/firestore
And check that your firebase.json
looks like the following:
{
"hosting": {
"public": "./public"
"rewrites": [
{
"source": "/ping",
"function": "ping"
}
]
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"hosting": {
"port": 5000
}
}
Note the rewrites
section. This makes /ping
handily available from the main Firebase Hosting domain, instead of the long and cryptic one such as us-central1-YOURFIREBASEPROJECT.cloudfunctions.net/ping
.
Before running tests, make sure to launch the emulators for Functions, Firestore, and Hosting:
npm run firebase -- emulators:start --project MYPROJECT
In the above command, npm run firebase
works because of the run script definition. Also, substitute the name of your Firebase project accordingly. If the setup is correct, your terminal should show something like:
emulators: Starting emulators: functions, hosting
hub: emulator hub started at http://localhost:4400
functions: functions emulator started at http://localhost:5001
hosting: Serving hosting files from: ./
hosting: Local server: http://localhost:5000
hosting: hosting emulator started at http://localhost:5000
functions[ping]: http function initialized
emulators: All emulators started, it is now safe to connect.
At this point, if you point your browser to localhost:5000/ping
, you should get the OK message (followed by the number representing the timestamp as Unix epoch). Of course, running the full tests (npm test
) should also yield in a successful run.
When setting up the tests for CI (continuous integration), it might be easier to let the emulators run the test automatically. Here is how it is done:
npm run firebase -- emulators:exec "npm test" --project MYPROJECT
The exec
option run the subsequent command, in this case the usual npm test
, after starting the emulators. Once the command is completed (whether successfully or not), the emulators are automatically terminated. This is perfect for the CI run!
Next trick on our sleeve: fixtures for Firestore. Let us assume that your application uses this NoSQL datastore via this simple function for illustration (and do not forget to add a new URL rewrite for /answer/
):
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
exports.answer = functions.https.onRequest(async (request, response) => {
try {
const doc = await db.collection('universe').doc('answer').get();
const value = doc.data().value;
console.log(`Answer is ${value}`);
response.send(`Answer is ${value}`);
} catch (err) {
console.error(`Failed to obtain the answer: ${err.toString()}`);
response.send(`EXCEPTION: ${err.toString()}`);
}
});
And the corresponding test:
it('should give a proper answer', async function () {
const res = await axios.get('http://localhost:5000/answer');
const answer = res.data;
expect(answer).toEqual('Answer is 42');
});
Launching the emulators (using the previous instructions) and running the tests however will result in a failure. And if you go to localhost:5000/answer, you fill discover an expected response:
EXCEPTION: TypeError: Cannot read property 'value' of undefined
This should not come as a surprise. When Firebase Emulators launched, its database (for Firestore) is empty. Hence, there is still no proper document, let alone a collection. It will be unnecessarily tedious to populate the database (it works for this simple example but a real-world app might have tons of collections and documents). How do we prepare a fixture for this?
Well, again the Firestore emulators to the rescue! While it is running, and you can perform another steps to populate the database (outside the scope of this blog post, perhaps we will discuss in some other time), you can snapshot the database and save it as the test fixture:
npm run firebase -- emulator:export spec/fixture --project MYPROJECT
Once the fixture is available, rerun the emulator (either as start
or through exec
) with the import
option and the Firestore database will not be empty anymore, as it is populated with the previous snapshot.
npm run firebase -- emulators:start --import spec/fixture --project MYPROJECT
Last but not least, let us run this test as an automation workflow using GitHub Actions. All you need is a file named .github/workflow/test.yml
with the following content:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- run: npm ci
- run: npm run firebase -- emulators:exec "npm test" --import spec/fixture
env:
CI: true
As it turns out, it is not too difficult to set up some practical tests of a Firebase project!