DEV Community

yosan
yosan

Posted on

Test with Firestore Emulator on GitHub Actions

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.

  "scripts": {
    "test": "firebase emulators:exec —only firestore 'jest --silent'"
  },
Enter fullscreen mode Exit fullscreen mode

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.

src/functions
├── index.ts
├── tsconfig.json
├── __tests__
├── firestore <- Not Next.js functions
└── web <- Next.js function
Enter fullscreen mode Exit fullscreen mode

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.

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

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".

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}
Enter fullscreen mode Exit fullscreen mode

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.

const client = algoliasearch(
  functions.config().algolia?.app_id ?? '',
  functions.config().algolia?.admin_key ?? ''
)
Enter fullscreen mode Exit fullscreen mode

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

Finally

It works!

20210305060628

Top comments (0)