💡 Prerequisites
Firebase Project. Not covered in this tutorial, but for deploying functions the Firebase projects needs to be on the Blaze Plan
This tutorial will cover the initial setup of a new project to the point where we implement our first Firebase Function by following the Test Driven Development methodology. This will setup the proper foundations for your project to grow on.
Let's vamos!
In the terminal, run the following:
mkdir myProject && cd myProject
firebase init functions && cd functions
When running 'firebase init functions' follow the instructions on the screen to select a Firebase project you want to use for this demo and make sure to use TypeScript.
$> npm install --save-dev jest @types/jest ts-jest typescript
$> npm install --save express @types/express
Add jest.config.js
to your functions folder
// jest.config.js
module.exports = {
"roots": [
"<rootDir>/src"
],
"testMatch": [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
}
Please note that all your test files need to be under the functions/src folder
Edit ./package.json
to contain
"scripts": {
"test": "jest"
}
Jest provides the global 'test' function and comes prebuilt with common assertions like 'expect', so we could write our first code like this:
// index.test.ts
test('test hello world', () => {
expect(true).toBe(false);
});
The first test is supposed to fail. If you've looked into Test Driven Development (TDD) in the past you know the philosophy of RED - GREEN - REFACTOR:
- write a failing test [RED]
- write just enough code to make the test pass [GREEN]
- refactor your code [REFACTOR].
If you run 'npm test' in the terminal you'll notice that Jest runs this test, so we can celebrate that our tests run, but in the next step we need to make the test more useful.
💡 Quick look into Express.js
To understand our next bit of code, we need to first look at what we're going to test. For this tutorial we're going to test an HTTP callable Firebase function <a href="https://firebase.google.com/docs/functions/http-events" target="_blank">docs</a>, which looks like the code we have commented out by default in ./src/index.ts
.
export const helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
});
We have the function ‘helloWorld’ that takes two argument: ‘request’ and ‘response’. Important to note here is that the arguments types are those from expressjs, <a href="https://expressjs.com/en/4x/api.html#req" target="_blank">Request</a> and <a href="https://expressjs.com/en/4x/api.html#res" target="_blank">Response</a> respectively.
The way onRequest functions work (i.e. helloWorld in our case) is that they take the first parameter ‘request’, then modify and send back the second parameter ‘response’.
In our test we will specify the contents of the ‘request’ and then assert our expectations with regards to the contents of ‘response’.
Let's write a test for the greeting function. When we provide a name (i.e. Jeff) we want the function to return a personalized greeting (i.e. Hello Jeff).
Our test file will in the end look like this
// index.test.ts
import * as express from 'express'; // 1.
import { greetings } from '../index'; // 2.
test('test personalized greeting', () => {
const req = { body: { "name": "Tony" } }; // 3.
const res = {
send: (returnMessage: any) => {
expect(returnMessage).toBe("Hello Tony"); // 4.
}
}
greetings(req as express.Request, res as express.Response) // 5.
});
Let's go over each step to see what happens:
- import 'express' because we need the custom data types:
express.Request
andexpress.Response
- import the function that we want to test from
index.ts
, in our case it's called 'greetings' - build the request 'req'
- define the 'send' handler to contain our expectation (i.e. test)
- call the function with a given input and an expected output
Please note that after writing the import 'greetings' statement our code would not compile. This is in TDD is considered a failing test, and the next step would be to write just enough code to make it pass. For brevity this example skips those extra steps.
Initially the test would fail because we are trying to import 'greetings' from index.ts
, but there's nothing in index.ts
, as all the code is commented out. So let's just fix that and also rename the function to greetings.
Your index.ts
should look like this:
import * as functions from 'firebase-functions';
// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript
export const greetings = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
});
Great, now you can run your entire test suite (i.e. the one test we've written so far) by running the following command in the terminal:
$> npm test
As you'll see, the test fails, which is what we'd expect.
Expected: "Hello Tony"
Received: "Hello from Firebase!"
The only thing now left to do, is:
Going back to your index.ts file, to make the test pass you'll need to change the greetings function to look like below:
export const greetings = functions.https.onRequest((request, response) => {
const name = request.body.name;
response.send(`Hello ${name}`);
});
You can run the test again (after saving the changes) and notice that the test passes.
Congratulations! You've now written your first function in a TDD manner and can now expand your project with confidence.
TDD can be a bit difficult in the beginning but pays dividends over time. Not only does it make code a pleasure to maintain and refactor, but it also improves the design of your project without you even realizing it (aka for FREE). TDD is awesome, is what I'm trying to say, and you should invest in the initial learning curve because it gets easier and makes your life more enjoyable in the long run.