How to Run JEST in Github Actions with Firestore
Tags: javascript, firebase, githubactions, testing
Published: 2023-07-14 19:32:06

This post will guide you through the steps of running automated tests using Jest with GitHub Actions.

## Real Case Scenario

We have an API built in NodeJS that was returning false responses or causing errors due to a lack of tests. This issue makes it difficult to debug or add new features to the codebase, since the behavior is not tested and cannot be guaranteed to work as expected.

So, the codebase needs to be covered with tests to ensure that endpoints are returning the expected results. These tests should be run automatically when creating a PR to ensure that all edge cases are covered and that no logical errors will occur in production.

It's not only limited to tests, but it's also a good practice to apply a few pipelines to the PR to ensure that all linting checks and test results are successfully passed.

In addition, the codebase uses Firestore as a NoSQL database to store all necessary data. This means that there are Firestore queries in the codebase that need to be handled in the tests. Mocking the queries is not a good practice since testing the query is a crucial part of verifying its functionality.

Using the development database directly is also not a good option since it can mutate the development database and interfere with all documents, which can break the staging environment and make it difficult to maintain.

Furthermore, another question arises regarding how to run it in GitHub actions if we are not going to use the development database instance.

## Setting Up Project

I have already created a sample NodeJS API to focus solely on testing and GitHub actions. You can clone the repository (init branch) as follows:

{% embed https://github.com/thepylot/jest-firestore-workflows %}

The initial state of the project includes basic configurations and dependencies to get started. 

Next, we will use a Docker image to run the Firestore emulator locally. This also will be used in Github Workflows to start the emulator on-the-fly and use it as a test database.

Create a `docker-compose-test.yml` in root level of the project:

**docker-compose-test.yml**

```yaml
version: "3.2"
services:
  firestore_emulator:
    image: mtlynch/firestore-emulator
    environment:
      - FIRESTORE_PROJECT_ID=my-blog-test
      - PORT=8080
    ports:
      - 8080:8080
```

Now let’s run the docker-compose with following command:

`docker-compose -f docker-compose-test.yml up -d
`
You will be able to see a container running the emulator, which is ready to use in your code.

## Configuring Jest

Now that our emulator is up and running, it's time to configure Jest before starting the actual coding.

If you navigate to `services/db.js`, you will see the initialization of a Firestore instance based on the environment setup.

Environment variables are typically stored in `.env` files or populated through deployment platforms. For tests, it is not necessary to use actual environment variables since the emulator is running in a Docker container with dummy data. We don't need to push or use the `.env` file for a few test variables because Jest allows us to mock environment variables before running tests.

To mock testing environment variables, create a directory named **.jest** at the root level and add a new file inside it named `setEnvVars.js`.

**.jest/setEnvVars.js**

```javascript
process.env.NODE_ENV = 'dev';
process.env.TEST_ENABLED = true;
process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080"
process.env.FIRESTORE_PROJECT_ID = "my-blog-test"
```

`TEST_ENABLED` will separate the initialization of Firestore for testing environments, but not for actual staging or production, as defined in the file `services/db.js`.

We set the value of `FIRESTORE_EMULATOR_HOST` to `localhost:8080` because we mapped it in the docker-compose file. This means that the emulator will be exposed to localhost on port `8080`.

Finally, the `FIRESTORE_PROJECT_ID` points to the name of the Firestore project ID that we also set in the docker-compose file for the emulator.

The next step is to add a configuration file in root level named `jest.config.js`. This file will let Jest know that there are setup files that need to be populated.

**jest.config.js**

```javascript
module.exports = {
    setupFiles: ["<rootDir>/.jest/setEnvVars.js"]
  };
```

Also, add another file named `jest.setup.js` to define max limit of timeout for tests. This will allow us to debug test without having timeout issue:

**jest.setup.js**

```javascript
jest.setTimeout(50000);

```

Lastly, update your `package.json` scripts by adding the following line to run tests:

```json
"test": "jest"
```

If you run `npm run test` now, it will attempt to run tests where none exist yet.

## Adding tests

We have an `/posts` endpoint that retrieves data from Firestore and needs to be covered with tests. To begin, create a tests directory and add a new file named `handler.test.js`.

Then, let’s add our first test case:

**tests/handler.test.js**

```javascript
const { handler } = require("../handler");
const  { db } = require("../services/db");
const { faker } = require("@faker-js/faker");
const request = require('supertest');

function postsGenerator() {
    let posts = []
    const postTitles = Array.from({length: 5}, (_) => faker.random.word())
    postTitles.forEach(title => {
        posts.push(
            {
                title: title,
                timestamp: new Date(),
            }
            );
    })
    return posts;
  }

afterEach( async () => {
    await db
    .collection('posts')
    .get()
    .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
            doc.ref.delete();
        });
    });
}, 30000)

describe('GET /posts', () => {
    it('GET /posts => retrieve posts', async () => {
        const posts = postsGenerator()
        const queryPromises = []
        posts.forEach((createdPost) => {
            queryPromises.push(db.collection("posts").doc().set(createdPost))
        })
        await Promise.all(queryPromises);

        return await request(handler)
        .get('/posts')
        .expect('Content-Type', /json/)
        .expect(200).then(response => {
            expect(response.body.posts.length).toEqual(5)
        })
    });
});
```

Now let's break down the code to see what applies here:

```javascript
const { handler } = require("../handler");
const  { db } = require("../services/db");
const { faker } = require("@faker-js/faker");
const request = require('supertest');
```

To begin testing, we need to import the required packages:

`handler` - where the endpoint is declared
`db`  - the initialized Firestore database
`faker` - a fake data generator library
`supertest` - a library that allows us to test endpoints using HTTP requests.

```javascript
function postsGenerator() {
    let posts = []
    const postTitles = Array.from({length: 5}, (_) => faker.random.word())
    postTitles.forEach(title => {
        posts.push(
            {
                title: title,
                timestamp: new Date(),
            }
            );
    })
    return posts;
  }
```

The above function is a helper function that generates and returns sample posts using **Faker**.

```javascript
afterEach( async () => {
    await db
    .collection('posts')
    .get()
    .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
            doc.ref.delete();
        });
    });
}, 30000)
```

In Jest, `afterEach` is a method that runs after each test case. It is used to clean up any resources that were created during the test and ensure that the environment is clean before the next test runs. 

In the given code, `afterEach` is used to delete all documents from the posts collection in Firestore after each test case is run, so that the tests do not interfere with each other.

Other methods, such as `beforeEach` or `afterAll`, can also be used to apply pre- and post-actions based on your needs.

```javascript
describe('GET /posts', () => {
    it('GET /posts => retrieve posts', async () => {
        const posts = postsGenerator()
        const queryPromises = []
        posts.forEach((createdPost) => {
            queryPromises.push(db.collection("posts").doc().set(createdPost))
        })
        await Promise.all(queryPromises);

        return await request(handler)
        .get('/posts')
        .expect('Content-Type', /json/)
        .expect(200).then(response => {
            expect(response.body.posts.length).toEqual(5)
        })
    });
});
```

We’re verifying that the `/posts` endpoint retrieves the expected number of posts from Firestore.

It performs the following steps:

- Generates 5 sample post objects using a helper function `postsGenerator`.
- Iterates through the posts and pushes a Firestore set method promise to an array of query promises.
- Uses `Promise.all` to wait for all the queries to finish executing.
- Makes a `GET` request to the `/posts` endpoint using supertest.
- Expects the response to have a `Content-Type` of JSON and a status code of `200`.
- Expects the response body to have an array of posts with a length of `5`.
- Now run your tests with `npm run test` to verify that all the results have passed.

If you want to debug tests using VSCode, you can use the following `launch.json` file:

```json
{
    "version": "0.2.0",
    "configurations": [
		{
			"type": "node",
			"request": "launch",
			"name": "Jest: current file",
			"env": { 
				"NODE_ENV": "dev",
                "TEST_ENABLED": "true",
                "FIRESTORE_PROJECT_ID": "my-blog-test",
                "FIRESTORE_EMULATOR_HOST": "localhost:8080"
			},
			"program": "${workspaceFolder}/node_modules/.bin/jest",
			"console": "integratedTerminal",
			"windows": {
			  "program": "${workspaceFolder}/node_modules/jest/bin/jest"
			}
		}
    ]
}
```

## Adding Github Workflows

Now that we have tests, it's time to automatically run them when creating a PR and ensure that all tests pass before merging.

Create a new folder named `.github` and, inside it, another folder named workflows. All Github workflows must be stored in this specific directory.

Next, Create a new file called `test.yml` inside the workflows directory to hold the YAML file of the workflow steps:

**.github/workflows/test.yml**

```yaml
name: Test

on:
  pull_request:

jobs:
  tests:
    name: Running Tests
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v1

    - name: Start containers
      run: docker-compose -f "docker-compose-test.yml" up -d --build

    - name: Install node
      uses: actions/setup-node@v1
      with:
        node-version: 16.x

    - name: Install dependencies
      run: npm install

    - name: Run tests
      run: npm run test

    - name: Stop containers
      if: always()
      run: docker-compose -f "docker-compose-test.yml" down
```

The workflow will run automatically when a pull request is created, and the results will be displayed in the pull request page.

Since we are running an emulator inside the Docker container, it is very simple to run a workflow without additional configuration for Firestore. Everything comes in a container already.

Special shoutout to the author of [https://github.com/mtlynch/firestore-emulator-docker](https://github.com/mtlynch/firestore-emulator-docker) for creating such a useful tool with great flexibility!
The source code and final version can be found in the repository below:

{% embed https://github.com/thepylot/jest-firestore-workflows %}

## Troubleshooting Tips

Before running tests see the logs of container to see is if Firestore is up and running.

If tests are failing, try restarting the container to run fresh tests to avoid any unresolved query executions.

## Conclusion

In this post, we have seen how to run automated tests using Jest with GitHub Actions. We also covered how to use Firestore as a NoSQL database to store all necessary data and how to run it in GitHub actions with a Firestore emulator.

After setting up the project, we configured Jest before starting the actual coding. We then added tests to our /posts endpoint, verified that they ran correctly, and debugged them if needed.

Finally, we added Github Workflows to automatically run tests when creating a pull request and ensure that all tests pass before merging.

We hope this post has helped you set up automated tests for your NodeJS API using Jest with GitHub Actions and Firestore.