Writing your first Firebase Function unit test

Writing your first Firebase Function unit test

💡 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!

1. Setup Firebase Functions project locally

In the terminal, run the following:

1
2
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.

2. Install Jest & express

1
2
npm install --save-dev jest @types/jest ts-jest typescript
npm install --save express @types/express

3. Configure jest

Add jest.config.js to your functions folder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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

4. Add script for running your tests with jest

Edit ./package.json to contain

1
2
3
"scripts": {
	"test": "jest"
}

5. Write a placeholder test in './src/tests/index.test.ts'

Jest provides the global ‘test’ function and comes prebuilt with common assertions like ‘expect’, so we could write our first code like this:

1
2
3
4
5
// 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:

  1. write a failing test [RED]
  2. write just enough code to make the test pass [GREEN]
  3. 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.

6. Write your first 'real' test

💡 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 docs, which looks like the code we have commented out by default in ./src/index.ts.

1
2
3
4
// 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, Request and Response 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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:

  1. import ‘express’ because we need the custom data types: express.Request and express.Response
  2. import the function that we want to test from index.ts, in our case it’s called ‘greetings’
  3. build the request ‘req’
  4. define the ‘send’ handler to contain our expectation (i.e. test)
  5. 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.

7. Run the complete test by switching to your terminal and running:

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:

1
2
3
4
5
6
7
8
9
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:

1
npm test

As you’ll see, the test fails, which is what we’d expect.

1
2
Expected: "Hello Tony"
Received: "Hello from Firebase!"

The only thing now left to do, is:

8. Write the code to make the test pass

Going back to your index.ts file, to make the test pass you’ll need to change the greetings function to look like below:

1
2
3
4
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.

9. Celebrate 🎉

Congratulations! You’ve now written your first function in a TDD manner and can now expand your project with confidence.

Conclusions

TDD is a bit difficult in the beginning but pays dividends over time. Not only does it make code a please to maintain and refactor, but it also improves the design of your project without you even realizing it. 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.

  • firebase
  • software development