In this post we’re going to look at how we can create fakes to test Go code that interacts with Firestore. I’m going to refer to this part of the application as a repository, but it could be any code that stores or fetches data in a Firestore database.

Originally written in August 2021, I never got around publishing this post. I’ve had a quick glance at it, and the content seems decent enough, but keep in mind that some things may have changed since then…

Why bother testing repositories?

Depending on what architectural style you’re following in your application (e.g. Hexagonal architecture), you may already have some type of abstraction between your service logic and your Firestore repository implementation (e.g. ports & adapters). If so, you’re probably already keeping your repositories lean & with as little logic as possible, and you’re able to easily mock your repositories and test the business logic in your services. So why would you bother to test the repository implementation at all?

Well, sometimes life isn’t as clean and simple as that, and you may need a bit of logic in your repositories. Or you’re following a completely different architectural pattern, and keep all of your logic together with your data layer (hey, I’m not judging). Perhaps you simply want to test as much as possible, and get that sweet 100% test coverage badge (good for you).

Whatever the reason, you have a few options available to you. You could start a Firestore emulator and just connect the normal Firestore client to the instance running locally. This is a very good option, and I wholeheartedly recommend it, but once we go down that route we need to start an external process before running our tests. That means we probably need to script a bunch of things outside of our tests, the tests will take longer to execute, and something will most likely bomb on the CI server sooner or later. Also, doing that we edging into integration test territory, rather than pure unit tests (not that it matters much).

So why can’t we just define an interface that looks like the part of the Firestore client that we use, and use that to create a mock in our tests? Well, we could, but the problem is that the functions of the Firestore client returns a lot of concrete types, which could make mocking it that way a real hassle, especially when you deal with complex stuff such as transactions. Google does have some tips on their Github page regarding testing with mocks, so you can check that out if you’re interested.

That very same page also covers another approach, which is the one I will cover in this post, and that is testing using fakes. While I will focus on testing Firestore code, the same principles can be used for almost all the Google clients.

gRPC Clients

Most of of the clients in Google’s Go libraries are based on gRPC. That means that when you use a client, such as the one for Firestore, it will make network calls using gRPC to the Google services. This is useful to us, because it means we can use that to implement fake in-memory gRPC servers that the clients will call instead.

In order to illustrate this, let’s first create a stupid little example repository that we’re going to test. Let’s say that we have a user database in Firestore and that the repository is used to interact with that data:

package fakes

import (
	"context"
	"time"

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

type User struct {
	Email     string
	Enabled   bool
	CreatedAt time.Time
}

func NewRepository(c *firestore.Client) *Repository {
	return &Repository{c}
}

type Repository struct {
	client *firestore.Client
}

func (r *Repository) GetUser(ctx context.Context, id string) (*User, error) {
	doc, err := r.client.Collection("users").Doc(id).Get(ctx)
	if err != nil {
		return nil, err
	}

	var user User
	if err := doc.DataTo(&user); err != nil {
		return nil, err
	}

	return &user, nil
}

Just so it’s said, the logic in these examples is not best practice, and I wouldn’t store or handle users in this way in a real world application. The code itself it just meant as a backdrop to the testing, which is what we’re focusing on today.

Here we have a simple DTO struct to represent our user, and a bare bones repository to query for users from a Firestore database. As you can see, the NewRepository takes a concrete Firestore client, not an interface, and like I mentioned earlier, the reason is that while we could write an interface mimicking the client, it would still be riddled with lots of Firestore details, so it actually don’t help us that much. But if you wanted to do it by the book, you could absolutely define and use an interface here instead (again, good for you).

Faking the server

So how do we go about to actually test this thing? Again, this is described in the Google docs, but basically every type of service in the Google libraries also have an empty implementation of it’s server counterpart. Typically they are called UnimplementedXXXServer, where XXX is replaced with the particular service (Firestore in our case). This is part of the autogenerated gRPC code that’s hidden within the libraries, and these empty implementations fulfills the full interface of the server, but without any actual logic. So calling any function of these servers will fail with an error, as we will see.

The empty server we are interested in is called UnimplementedFirestoreServer, so it’s just a matter of finding the path to import it and then embed it into a struct of our own:

import firestorepb "google.golang.org/genproto/googleapis/firestore/v1"

type fakeFirestoreServer struct {
	firestorepb.UnimplementedFirestoreServer
}

We import the package using the firestorepb alias in order to differentiate it from the ordinary firestore client package which we will use alongside it (the pb comes from Protocol Buffers, which is used by gRPC). Thanks to the embedding, fakeFirestoreServer is now a fully working server (well, it’s not actually working, but it is compiling) that we can use to create a Firestore client:

import (
	"context"
	"net"

	"cloud.google.com/go/firestore"
	"google.golang.org/api/option"
	firestorepb "google.golang.org/genproto/googleapis/firestore/v1"
	"google.golang.org/grpc"
)

func newFakeClient(ctx context.Context, srv firestorepb.FirestoreServer) (_ *firestore.Client, cleanup func(), _ error) {
	l, err := net.Listen("tcp", "localhost:0")
	if err != nil {
		return nil, nil, err
	}

	gsrv := grpc.NewServer()
	firestorepb.RegisterFirestoreServer(gsrv, srv)
	fakeServerAddr := l.Addr().String()

	go func() {
		if err := gsrv.Serve(l); err != nil {
			panic(err)
		}
	}()

	c, err := firestore.NewClient(ctx, "dummy-project",
		option.WithEndpoint(fakeServerAddr),
		option.WithoutAuthentication(),
		option.WithGRPCDialOption(grpc.WithInsecure()))

	return c, gsrv.Stop, err
}

Woah, there’s quite a lot going on here in this little function, but it’s pretty straightforward, so bare with me. In order to create a client we need a Context and a server implementation, so those are the arguments. The return values are the resulting Firestore client, a cleanup function (the named return value here is optional, but I find it adds a bit of self-documentation), and any potential error.

We start with creating a TCP network listener, which listens on the loopback network interface (the 0 means we need any old port). Then we create a gRPC server and register our own fake Firestore server. This gives us a (local) network address that we can use to connect our client. Then we run a Go routine that starts the gRPC server so that it’s listening for traffic (this is the reason we need to return the cleanup function).

Finally, we create the actual Firestore client using the usual library, but with some pretty unusual options. As we don’t care about the project ID in our fakes tests I simply pass a dummy string. Then we use the address we got from our locally running gRPC server, disable authentication and SSL, and return everything so that we can use it in our tests:

func TestGetUser(t *testing.T) {
	ctx := context.Background()

	srv := &fakeFirestoreServer{}
	client, cleanup, err := newFakeClient(ctx, srv)
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	r := NewRepository(client)
	user, err := r.GetUser(ctx, "abc")
	if err != nil {
		t.Fatal(err)
	}
	if user == nil {
		t.Fatal("missing user")
	}
}

Again, the steps are very straightforward:

  1. Create a fake server
  2. Use the server to create a fake client
  3. Do not forget to defer cleaning up the fake server
  4. Create a repository using the client
  5. Test the repository

I’ll admit it’s not the best test, but it illustrates the point. So what happens if we run the test?

=== RUN   TestGetUser
--- FAIL: TestGetUser (0.00s)
    repository_test.go:27: rpc error: code = Unimplemented desc = method BatchGetDocuments not implemented
FAIL
FAIL    github.com/hedlund/firestore-fakes      0.010s
FAIL

I do believe I mentioned this before; the UnimplementedFirestoreServer does not actualy contain any implementation, and using it will result in errors like this. Thats is, until we provide the implementation for the functions that we use.

Implementing a fake response

The error message above gives us all the information necessary to fic the problem; we need to implement the BatchGetDocuments function for our fake server. Unfortunately, implementing working fakes means that you’ll need to dig through the source code of the Google libraries, but it never hurts to get a better understanding of what happens under the hood, right? And you don’t need to dig that deep, as we simply need a solution that works for our tests (this is not a reusable library after all):

func (s *fakeFirestoreServer) BatchGetDocuments(req *firestorepb.BatchGetDocumentsRequest, srv firestorepb.Firestore_BatchGetDocumentsServer) error {
	return srv.Send(&firestorepb.BatchGetDocumentsResponse{
		Result: &firestorepb.BatchGetDocumentsResponse_Found{
			Found: &firestorepb.Document{
				Name: req.Documents[0],
				Fields: map[string]*firestorepb.Value{
					"Email": {
						ValueType: &firestorepb.Value_StringValue{StringValue: "abc@hedlund.xyz"},
					},
					"Enabled": {
						ValueType: &firestorepb.Value_BooleanValue{BooleanValue: true},
					},
					"CreatedAt": {
						ValueType: &firestorepb.Value_TimestampValue{TimestampValue: timestamppb.New(time.Now())},
					},
				},
				CreateTime: timestamppb.Now(),
				UpdateTime: timestamppb.Now(),
			},
		},
	})
}

This is pretty much what the Google services return to client library, and it is pretty verbose. But this way we have a good control over what is returned when the request is made, and we can take a few shortcuts (such as always expecting to return a single document). We also must remember to return the Name of the requested document (it is a typical Google URI style ID including the projectd ID), otherwise the client won’t be able to map it to the correct response. The timestamppb package is imported from google.golang.org/protobuf/types/known/timestamppb.

So what happens if we run the test?

=== RUN   TestGetUser
--- PASS: TestGetUser (0.00s)
PASS
ok      github.com/hedlund/firestore-fakes      0.012s

Yay, great success!

Fluent API

Normally we want to test different scenarios, and error situations etc. That means we need to be able to parametrize our fake server. Personally I’m a big fan of fluent interface APIs so I’m going to use that style here, but you can write your own code any way you like.

Instead of always returning the same user, let’s add a function to control what our server returns:

func newFakeServer() *fakeFirestoreServer {
	return &fakeFirestoreServer{}
}

type fakeFirestoreServer struct {
	firestorepb.UnimplementedFirestoreServer
	doc *firestorepb.Document
}

func (s *fakeFirestoreServer) WithUser(user *User) *fakeFirestoreServer {
	s.doc = &firestorepb.Document{
		Fields: map[string]*firestorepb.Value{
			"Email": {
				ValueType: &firestorepb.Value_StringValue{StringValue: user.Email},
			},
			"Enabled": {
				ValueType: &firestorepb.Value_BooleanValue{BooleanValue: user.Enabled},
			},
			"CreatedAt": {
				ValueType: &firestorepb.Value_TimestampValue{TimestampValue: timestamppb.New(user.CreatedAt)},
			},
		},
		CreateTime: timestamppb.Now(),
		UpdateTime: timestamppb.Now(),
	}
	return s
}

Now we can seed our server with test data using srv.WithUser(user), and the fluent API also means we can chain additional configuration options once we add more. I also add a newFakeServer “constructor” function, as I think it reads better when I use it for chaining. As we store the user document on the server, we also need to return it in our faked function:

func (s *fakeFirestoreServer) BatchGetDocuments(req *firestorepb.BatchGetDocumentsRequest, srv firestorepb.Firestore_BatchGetDocumentsServer) error {
	if s.doc != nil {
		s.doc.Name = req.Documents[0]
		return srv.Send(&firestorepb.BatchGetDocumentsResponse{
			Result: &firestorepb.BatchGetDocumentsResponse_Found{
				Found: s.doc,
			},
		})
	}

	return srv.Send(&firestorepb.BatchGetDocumentsResponse{
		Result: &firestorepb.BatchGetDocumentsResponse_Missing{
			Missing: req.Documents[0],
		},
	})
}

Again, you can see we cheat a bit and overwrite the document name before returning it, but I’ve also added a bit of logic to respond differently if we do not set a user in our fake server. That way it will respond with an appropriate missing response, and the client will behave much more normal.

So let’s create a table driven test to try it out:

func TestGetUser(t *testing.T) {
	tests := []struct {
		name     string
		server   *fakeFirestoreServer
		expFound bool
	}{
		{
			name: "found",
			server: newFakeServer().WithUser(&User{
				Email:     "abc@hedlund.xyz",
				Enabled:   true,
				CreatedAt: time.Now(),
			}),
			expFound: true,
		},
		{
			name:     "notfound",
			server:   newFakeServer(),
			expFound: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()

			client, cleanup, err := newFakeClient(ctx, tt.server)
			if err != nil {
				t.Fatal(err)
			}
			defer cleanup()

			r := NewRepository(client)
			user, err := r.GetUser(ctx, "abc")
			if err != nil && !tt.expFound && status.Code(err) != codes.NotFound {
				t.Fatal(err)
			}
			if user == nil && tt.expFound {
				t.Fatal("missing user")
			}
		})
	}
}

Again, this is not the best test ever, and our repository implementation is a pretty horrible abstraction as it leaks gRPC errors. But since we get those errors, we can use the gRPC status code to check if the error actually means the user is not found, and use that as a critera in our test. To reiterate - I wouldn’t do it this way in a real world example; this is just an illustration!

If we run the tests we can see that it works:

=== RUN   TestGetUser
=== RUN   TestGetUser/found
=== RUN   TestGetUser/notfound
--- PASS: TestGetUser (0.01s)
    --- PASS: TestGetUser/found (0.00s)
    --- PASS: TestGetUser/notfound (0.00s)
PASS
ok      github.com/hedlund/firestore-fakes      0.016s

Adding complexity

So far our tests hasn’t really made much sense, since our repository does not contain any logic, and we’re pretty much just testing the fake server (which is a bad thing™). Let’s add a bit complexity by adding a function to create a user, and to make things a bit more interesting, it’ll send a verification email as well, and wrap it all into a transaction, insuring that the user is not created if we fail to send the email.

To accomplish this, we extend our repository to take a Sender interface, which will be used to send the verification email:

type Sender interface {
	SendVerificationEmail(ctx context.Context, email string) error
}

func NewRepository(c *firestore.Client, s Sender) *Repository {
	return &Repository{c, s}
}

type Repository struct {
	client *firestore.Client
	sender Sender
}

Then we add the actual create function:

func (r *Repository) CreateUser(ctx context.Context, email string) (string, error) {
	id := uuid.NewString()
	ref := r.client.Collection("users").Doc(id)
	err := r.client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
		_, err := ref.Set(ctx, &User{
			Email:     email,
			Enabled:   false,
			CreatedAt: time.Now().UTC(),
		})
		if err != nil {
			return err
		}

		return r.sender.SendVerificationEmail(ctx, email)
	})
	if err != nil {
		return "", err
	}

	return id, nil
}

In order to generate an ID, I’ve added the github.com/google/uuid library here. Also note that the NewString function may panic, so don’t do it like this in production!

Nothing too special going on here, as long as we’re ignoring the fact that we’re sending emails as part of a repository implementation, but never mind that. We create a random UUID, run a transaction to set a new user object in the database, then send the verification email. The important bit here is that if the email bit at the end fails and we’re returning an error, the whole transaction should be rolled back and the user shouldn’t be created. It could be argued that the best way to test this would be to use an integration test (perhaps using the emulator), but here we’re going to use our fake server again.

In order to test our extended repository, we need to implement a fake email sender:

type fakeSender struct {
	err error
}

func (s *fakeSender) SendVerificationEmail(ctx context.Context, email string) error {
	return s.err
}

A simple test looks very similar to our first one:

func TestCreateUser(t *testing.T) {
	ctx := context.Background()

	client, cleanup, err := newFakeClient(ctx, newFakeServer())
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	r := NewRepository(client, &fakeSender{})
	id, err := r.CreateUser(ctx, "abc@hedlund.xyz")
	if err != nil {
		t.Fatal(err)
	}
	if id == "" {
		t.Fatal("missing id")
	}
}

And running the test yield similar results:

=== RUN   TestCreateUser
--- FAIL: TestCreateUser (0.00s)
    repository_test.go:88: rpc error: code = Unimplemented desc = method BeginTransaction not implemented
FAIL
FAIL    github.com/hedlund/firestore-fakes      0.017s
FAIL

As we’re using a new function, BeginTransaction, we need to add that to our fake server. Luckily, we can get away with a pretty barebones implementation:

func (*fakeFirestoreServer) BeginTransaction(context.Context, *firestorepb.BeginTransactionRequest) (*firestorepb.BeginTransactionResponse, error) {
	return &firestorepb.BeginTransactionResponse{}, nil
}

Running the tests again reveals that we’re missing the Commit implementation (makes sense), but before we add that, let’s make it possible to test commit errors by adding another fluent function:

type fakeFirestoreServer struct {
	firestorepb.UnimplementedFirestoreServer
	doc       *firestorepb.Document
	errCommit error
}

func (s *fakeFirestoreServer) WithCommitError(err error) *fakeFirestoreServer {
	s.errCommit = err
	return s
}

Then we can simply return that error as part of our Commit implementation:

func (s *fakeFirestoreServer) Commit(context.Context, *firestorepb.CommitRequest) (*firestorepb.CommitResponse, error) {
	return &firestorepb.CommitResponse{
		WriteResults: []*firestorepb.WriteResult{
			{},
		},
	}, s.errCommit
}

At this point, our basic test runs green, since we don’t inject any errors, nor test any of the interesting bits. To set the stage for the real tests, we need to add a few more functions. First, once a transaction fails, the server will need to implement the Rollback function. In order to check if this happens, I’m adding two simple booleans - one to keep track if the rollback actually happened, and one to mark that we expect it to happen:

type fakeFirestoreServer struct {
	firestorepb.UnimplementedFirestoreServer
	doc         *firestorepb.Document
	errCommit   error
	expRollback bool
	gotRollback bool
}

func (s *fakeFirestoreServer) ExpectRollback() *fakeFirestoreServer {
	s.expRollback = true
	return s
}

func (s *fakeFirestoreServer) Rollback(context.Context, *firestorepb.RollbackRequest) (*emptypb.Empty, error) {
	s.gotRollback = true
	return &emptypb.Empty{}, nil
}

I’m also going to add a helper function to validate that any conditions I set in my fake server is actually met:

func (s *fakeFirestoreServer) Validate(t *testing.T) {
	if s.expRollback != s.gotRollback {
		t.Errorf("unexpected rollback status, exp: %t, got: %t", s.expRollback, s.gotRollback)
	}
}

Then I update the fakeSender to keep track of the email it receives in a similar manner:

type fakeSender struct {
	err error
	exp string
	got string
}

func (s *fakeSender) SendVerificationEmail(ctx context.Context, email string) error {
	s.got = email
	return s.err
}

func (d *fakeSender) Validate(t *testing.T) {
	if d.exp != d.got {
		t.Errorf("send email mismatch, exp: %s, got: %s", d.exp, d.got)
	}
}

Now we’re set to write a suite of slightly better tests for the create function:

func TestCreateUser(t *testing.T) {
	errTest := errors.New("boom")

	tests := []struct {
		name   string
		email  string
		server *fakeFirestoreServer
		sender *fakeSender
		expErr bool
	}{
		{
			name:   "success",
			email:  "abc@hedlund.xyz",
			server: newFakeServer(),
			sender: &fakeSender{
				exp: "abc@hedlund.xyz",
			},
		},
		{
			name:   "commit-error",
			email:  "abc@hedlund.xyz",
			server: newFakeServer().WithCommitError(errTest).ExpectRollback(),
			sender: &fakeSender{},
			expErr: true,
		},
		{
			name:   "send-error",
			email:  "abc@hedlund.xyz",
			server: newFakeServer().ExpectRollback(),
			sender: &fakeSender{
				err: errTest,
				exp: "abc@hedlund.xyz",
			},
			expErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()

			client, cleanup, err := newFakeClient(ctx, tt.server)
			if err != nil {
				t.Fatal(err)
			}
			defer cleanup()

			r := NewRepository(client, tt.sender)
			id, err := r.CreateUser(ctx, tt.email)
			if err != nil && !tt.expErr {
				t.Fatal(err)
			}
			if id == "" && !tt.expErr {
				t.Fatal("missing id")
			}

			tt.server.Validate(t)
			tt.sender.Validate(t)
		})
	}
}

As you can see we can use the fluent interface to seed our fake server with different scenarios and expectations, and similarly configure conditions and behaviours for other dependencies (e.g. the sender). Whilst these tests are still quite basic, and especially the error checking is sub-par (it would be better to use proper error wrapping and checking), I think it still illustrates the possibilities we have to create some quite interesting test scenarios.

Not just Firestore

Like I mentioned in the beginning, using fakes in this way is not limited to just Firestore. Most of the clients in the Google libraries are gRPC based and can be faked the same way. If you want another, probably better, example, you can check out the Google repository here, where they’re using fakes to test code using the translate API.

You can also find the source code from the post over on Github.