Unit Testing with Firestore Emulator and Go

Automate the gcloud Firestore Emulator

If you’re developing for Google’s Cloud Platform (often referred to as ‘GCP’) you’re probably more likely than the average developer to be using the Go language (often referred to as ‘Golang’ for searchability) and Cloud Firestore, the successor to their Cloud Datastore NoSQL managed service.

Firestore has amazing features, it works offline, it allows clients to subscribe for live updates and it’s incredibly priced. It’s well worth checking out if you haven’t already explored it.

Of course any code you write should be fully tested, especially code that is updating your database, and while you could just fire requests from your unit-tests to a live Firestore service that isn’t always ideal - although Firestore is fast, it’s still a remote service which means you need internet access to run the tests and they will be slightly slower. Firestore pricing is also usage-based so you could end up paying for reads and writes (although they do provide a generous daily allowance).

Fortunately, they now provide an emulator that you can run locally which is ideal for unit testing. Normally, you have to start it running and then you can setup an environment variable that your code will use to connect to it. But instead of doing that manually each time, why not automate it so the emulator starts and ends when the unit-tests run?

First of all, you need to install the emulator itself and check that you can run it from the command line using the gcloud tool.

See: https://cloud.google.com/sdk/gcloud/reference/beta/emulators/firestore

Notice that when it starts up it outputs information to the console about the host and port that it’s running on and that it’s started. We’re going to add a TestMain function to our Go code to start it in a separate process, look for those values and then terminate when all tests have run.

In Go, the TestMain function provides for test setup and teardown work, a way to run code before and after all your tests complete so it’s perfect for this kind of thing.

Here’s the code:

package main

import (
	"context"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"testing"

	"cloud.google.com/go/firestore"
)

func newFirestoreTestClient(ctx context.Context) *firestore.Client {
	client, err := firestore.NewClient(ctx, "test")
	if err != nil {
		log.Fatalf("firebase.NewClient err: %v", err)
	}

	return client
}

func TestMain(m *testing.M) {
	// command to start firestore emulator
	cmd := exec.Command("gcloud", "beta", "emulators", "firestore", "start", "--host-port=localhost")

	// this makes it killable
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

	// we need to capture it's output to know when it's started
	stderr, err := cmd.StderrPipe()
	if err != nil {
		log.Fatal(err)
	}
	defer stderr.Close()

	// start her up!
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}

	// ensure the process is killed when we're finished, even if an error occurs
        // (thanks to Brian Moran for suggestion)
	var result int
	defer func() {
		syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
		os.Exit(result)
	}()

	// we're going to wait until it's running to start
	var wg sync.WaitGroup
	wg.Add(1)

	// by starting a separate go routine
	go func() {
		// reading it's output
		buf := make([]byte, 256, 256)
		for {
			n, err := stderr.Read(buf[:])
			if err != nil {
				// until it ends
				if err == io.EOF {
					break
				}
				log.Fatalf("reading stderr %v", err)
			}

			if n > 0 {
				d := string(buf[:n])

				// only required if we want to see the emulator output
				log.Printf("%s", d)

				// checking for the message that it's started
				if strings.Contains(d, "Dev App Server is now running") {
					wg.Done()
				}

				// and capturing the FIRESTORE_EMULATOR_HOST value to set
				pos := strings.Index(d, FirestoreEmulatorHost+"=")
				if pos > 0 {
					host := d[pos+len(FirestoreEmulatorHost)+1 : len(d)-1]
					os.Setenv(FirestoreEmulatorHost, host)
				}
			}
		}
	}()

	// wait until the running message has been received
	wg.Wait()

	// now it's running, we can run our unit tests
	result = m.Run()
}

const FirestoreEmulatorHost = "FIRESTORE_EMULATOR_HOST"

The code is commented to explain what it should do. When you execute any unit tests using go test the emulator should be started automatically and then setup the environment variable that the Firestore client library will use to connect to it, before running the unit tests and finally terminating the emulator.

If you don’t want to see the emulator output in the console, you can comment out or remove the log.Printf statement that shows it.

Now, in your unit tests, you can easily create an instance of the Firestore client using something like:

ctx := context.Background()
client := newFirestoreTestClient(ctx)
defer client.Close()

Easy local testing without having to manually setup the emulator each time!