Test with Firestore Emulator on GitHub Actions
Tags: firebase, github, testing
Published: 2021-07-14 21:17:22

I'm developing a web site using Next.js hosted by Cloud Functions. It uses Firestore as main database. And some Firestore-Trigger functions exist.

Now, I wanted to test my functions and firestore.rules with Firestore Emulator. Due to  manage its codes in GitHub, I wanted to use GitHub Actions for test.

I tried some ways to find more simple way to achive it. As a result of various investigations, I think preparing all-in-one GitHub Actions instance and using `firebase emulators:exec` command is most simple way.

Here is how I setup auto testing system.

## Setup local test

First of all, write tests and setup configurations to be able to run test in my local machine. 

Here is what I committed.
https://github.com/yosan/tennico/pull/13/files

I'll explain some points in it.

### Use firebase emulators:exec

Add this setting to package.json.

```json
  "scripts": {
    "test": "firebase emulators:exec —only firestore 'jest --silent'"
  },
```

`firebase emulators:exec` is useful command. It downloads, launch, and stop Firebase Emulator automatically.

While executing tests, many Cloud Functions logs are shown on our console so that it's hard to read test results. So I use `jest --silent`.

### Devide Cloud Functions' index.ts

Usually we import an index.ts which exports all of Cloud Function's implementations to wrap `firebase-functions-test`. But if a function which serves Next.js is contained in it, it becomes difficult to import index.ts because Next.js needs many settings to do so. Therefore, I separated Next.js function and other functions. Only import functions except Next.js one.

```she’ll
src/functions
├── index.ts
├── tsconfig.json
├── __tests__
├── firestore <- Not Next.js functions
└── web <- Next.js function
```

### Mock algolia

My functions use algolia to register my Firestore document to index. Mocking alogolia client is little hard for me. This is how I mocked it.

```typescript
let mockSaveObject: ReturnType<typeof jest.fn>
jest.mock('algoliasearch', () => {
  mockSaveObject = jest.fn().mockReturnValue({})
  return {
    default: () => ({
      initIndex: () => ({
        saveObject: mockSaveObject,
      }),
    }),
  }
})
```

## Setup GitHub Actions

This is the diff of setup.
https://github.com/yosan/tennico/pull/14

### Workflow Settings

I made settings referring to Cloud Functions runtime environment. 

This was my first time to use GitHub Actions. I felt good because workflow settings was simple and I didn't have to write Dockerfile.

Firebase Emulator needs to project ID selection to launch and some credential is needed for it. I specified it from "GitHub > Settings > Secrets".

```yaml
name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-18.04
    env:
      FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
      GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: '10.18.1'
      - uses: actions/setup-java@v1
        with:
          java-version: '15.0.2'
          java-package: jdk
          architecture: x64
      - name: Install dependencies
        run: yarn
      - name: Test
        run: yarn test --project ${GCLOUD_PROJECT}
```

Before I wrote this settings, I found many articles which said using Docker Compose is needed to use Firebase Emulator on GitHub Actions.

Maybe there are some difference we can do between them. But I wanted to do was only to test Cloud Functions and firestore.rules and it seemed that it would become more complex by Docker Compose. So, I did't use it.

### functions.config()

If `.runtimeconfig` is pushed, `functions.config()` can return value. But we shouldn't do that if we published our code on public repository in GitHub.

I set default value to resolve runtime error. For example, when I want to refer algolia API key in Cloud Functions environment variables, I write code like this.

``` typescript
const client = algoliasearch(
  functions.config().algolia?.app_id ?? '',
  functions.config().algolia?.admin_key ?? ''
)
```

Ofcource, the client will return error when it calls Algolia API. But we can avoid it by mocking client.

## Finally

It works!

![20210305060628](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2lj4fbc4xo2w9xi32cp6.png)