Firebase Cloud Functions Jest Setup with TypeScript & Mocking

carlomigueldy

Carlo Miguel Dy

Posted on November 14, 2021

Firebase Cloud Functions Jest Setup with TypeScript & Mocking

Overview

As the backend requirements grows with 0% test coverage, the harder it is to maintain the project or even scale due to bugs coming out from anywhere. Debugging without tests also takes so much time to have to manually open the app, login using the credentials that you remember for a particular user role, and finally following the necessary steps to replicate the bug only to read off the error message or find out if it's working. It's a waste of time writing functionalities without tests, you didn't actually "save" time by not writing tests.

Introduction

We're only going to cover how to setup Jest with TypeScript and a basic unit test and fairly simple mocking to a class that we want to mock a response value of a certain method.

Setup Jest TypeScript

Setting up Jest on Cloud Functions is fairly simple and straight-forward. Just run the following command and you're ready to go!

npm i jest @types/jest ts-jest typescript -D
Enter fullscreen mode Exit fullscreen mode

The following are the dependencies that our Cloud Functions directory in functions directory requires. We'll break it down below of what they are and what's their purpose.

Dependencies:

  • jest a testing framework maintained by Facebook, works well with frontend frameworks like Vue, React, Angular also with TypeScript. We'll be using Jest for writing up all of our unit tests, integration tests, and other types of tests that you know of.
  • @types/jest all the TypeScript typings are found here, we're getting the auto complete sugar that we developers love
  • ts-jest there are some utility functions that with help us mock a few functions or methods from a third party library
  • typescript our favorite tool

Once all the dependencies are installed, then all is cool! Next up is we create a file called jest.config.js at the root of our functions directory and we'll have the following lines of code in the newly created file.

module.exports = {
  "roots": [
    "<rootDir>/src"
  ],
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}
Enter fullscreen mode Exit fullscreen mode

Now we'll add a script in our package.json file on key scripts

// package.json
{
    "scripts": {
        // ... other scripts
        "test": "jest"
    }
}
Enter fullscreen mode Exit fullscreen mode

Now by the following setup that we have, we will have all of our tests suites housed inside src directory. Here are the following examples on how the structure would look.

functions
    - src
        - todo
            - todo.service.ts
            - todo.spec.ts // or you can name it as "todo.test.ts"
        - todo-b
            - todo-b.service.ts
            - todo-b.spec.ts // or you can name it as "todo-b.test.ts"
Enter fullscreen mode Exit fullscreen mode

And when we want to execute or run our tests cases, we'll use the following command

jest # or you can run with "npm run test" or "npm test"
Enter fullscreen mode Exit fullscreen mode

You can read more for the full documentation from here

Writing our First Unit Test

Suppose that TodoService (relative path todo/todo.service.ts) is the class we want to test.

// todo.service.ts

export class TodoService {
    public giveMeANumber(number: number): number {
        return number;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we will proceed to create a test/spec file in the same directory as the service so we can easily find its corresponding test file.

// todo.service.spec.ts

import { TodoService } from './todo.service.ts'

describe('TodoService', () => {
    let service: TodoService

    beforeEach(() => {
        service = new TodoService()
    })

    describe('giveMeANumber', () => {
        test('when value is true, then should return true', () => {
            expect(true).toBe(true)
        })

        it('should return 5, when given parameter is 5', () => {
            const number = service.giveMeANumber(5)

            expect(number).toBe(5)
        })
    })
})
Enter fullscreen mode Exit fullscreen mode

Notice how we did not import describe, test, beforeEach, it, and expect functions because the setup that we have for Jest already handled it for us, the functions are available on all *.spec.ts or *.test.ts files. And lastly it's the @types/jest that will give us the convenience of auto-completes so we don't have to worry if we're passing down the correct arguments into those functions.

The describe function describes the test suite or it groups a set of test cases in it. It is very similar to group when we are writing tests in Flutter. And the test function is the actual test case.

And now to run our tests we will use the following command in the terminal. Suppose that you are in the root directory of the project, in this case it's ./functions

jest # `npm test` or `npm run test`
Enter fullscreen mode Exit fullscreen mode

Note: It will take about under 10s to run the test, not sure why it's this slow.

Then that should display the test results in your terminal.

When you want to run specific tests, you can use jest -t 'when value is true, then should return true' make sure it matches that test case you want to run.

Writing our First Test with Mocking

And now we are going to make mocks to any third party libraries that we depend on so we don't have to make the test await for any HTTP calls.

Say TodoService will have a dependency for AwesomeTodoAPI where we want to make a call only to retrieve outstanding todos. And we just want to assert the array length of the response data just to make an assertion example as simple as possible. The only thing we want to learn now is how we can create mocks in Jest with TypeScript.

Now let's have the same service class but is dependent to a library AwesomeTodoAPI where this dependency will handle the HTTP calls internally, so we're only calling methods from it that will give us the resources we require.

// todo.service.ts

import AwesomeTodoAPI from '@awesome/AwesomeTodoAPI'

type AwesomeTodo = {
    title: string
    description: string
}

export class TodoService {
    private awesomeTodoApi: AwesomeTodoAPI

    constructor() {
        this.awesomeTodoApi = new AwesomeTodoAPI()
    }

    public async fetchOutstandingTodos(): Promise<AwesomeTodo[]> {
        const response = await this.awesomeTodoApi.fetchOutstandingTodos()
        return response.data
    }

    public giveMeANumber(number: number): number {
        return number;
    }
}
Enter fullscreen mode Exit fullscreen mode

The method fetchOutstandingTodos inside our TodoService is dependent on the returned response from AwesomeTodoAPI method fetchOutstandingTodos. We can then proceed to creating a mock of the returned response for that. Without mocking, it will still make the actual call for fetchOutstandingTodos but that'll take some time before the data gets returned and will result into making our tests running slow, and that's not what we want in this case since we'll only want to unit test our code and not make an integration test.

On the same spec/test file that we have,

// todo.service.spec.ts

import { mocked } from "ts-jest/utils";
import { TodoService } from './todo.service.ts'
import { AwesomeTodoAPI } from '@awesome/AwesomeTodoAPI'

// Let Jest automatically create all the mocks for us
jest.mock('@awesome/AwesomeTodoAPI')

describe('TodoService', () => {
    let service: TodoService
    const mockedAwesomeTodoApi = mocked(AwesomeTodoAPI, true)

    beforeEach(() => {
        service = new TodoService()

        // Clear out all the mocks that we 
        // created before each test case
        mockedAwesomeTodoApi.mockClear()
    })

    describe('fetchOutstandingTodos', () => {
        test('when called, should return all the outstanding todos', async () => {
            // Arrange: Mock the returned response value
            AwesomeTodoAPI.prototype.fetchOutstandingTodos = jest.fn()
                .mockImplementationOnce(() => {
                    return {
                        data: [
                            { title: 'Todo A', description: 'lorem', },
                            { title: 'Todo B', description: 'lorem', },
                        ] as AwesomeTodo[]
                    } as AwesomeTodoAPIResponse
                })

            // Action
            const todos = await service.fetchOutstandingTodos()

            // Assert
            expect(todos.length > 1).toBeTruthy()
        })

        test('when there are no outstanding todos, should return empty array', async () => {
            // Arrange: Mock the returned response value
            AwesomeTodoAPI.prototype.fetchOutstandingTodos = jest.fn()
                .mockImplementationOnce(() => {
                    return {
                        data: [] as AwesomeTodo[]
                    } as AwesomeTodoAPIResponse
                })

            // Action
            const todos = await service.fetchOutstandingTodos()

            // Assert
            expect(todos.length === 0).toBeTruthy()
        })
    })
})
Enter fullscreen mode Exit fullscreen mode

Now to break it down for you on what's happening:

  • We imported AwesomeTodoAPI from '@awesome/AwesomeTodoAPI' and automatically created a mock for all of its method by calling jest.mock(importPath, factoryCallback) (You can learn more about the factoryCallback I am referring to from the official Jest docs if you wish to, basically you just have to match all method names and its return values, and have the callback return an object. It's obviously a boilerplate code but maybe there can be a potential use case for manually setting up mocks)
  • Next is we create a variable called mockedAwesomeTodoApi where this is the actual mock and thanks to the utility function mocked from ts-jest where we pass down in the first argument of the object we want to mock and its second argument is if we want to mock it deep so we set true
  • Inside beforEach is we setup our service TodoService and clear out all the mocks that was created previously from each test case, so before each test case will be a fresh mock by calling mockClear from the mocked object
  • Now inside our test suite for method fetchOutstandingTodos on the "Arrange" section is we setup the return response value of the AwesomeTodoAPI for its method fetchOutstandingTodos since we don't want to make an actual call to the API. The way we mock the returned response is by accessing the class prototype itself, and access fetchOutstandingTodos and assign a new value to it by what is returned in the callback function for mockImplementationOnce
  • Then we make the actual call from our TodoService that calls fetchOutstandingTodos from AweomseTodoAPI and pass down the returned value to the todos variable
  • Finally we assert if todos contains some data or if it's empty.

Wrapping Up

We learned how to setup Jest with TypeScript in just a few minutes. Now we can get our tests up and running in no time!

Thanks for taking the time to read. Good day!

💖 💪 🙅 🚩
carlomigueldy
Carlo Miguel Dy

Posted on November 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related