1# Testing Code that depends on Go Client Libraries
2
3The Go client libraries generated as a part of `cloud.google.com/go` all take
4the approach of returning concrete types instead of interfaces. That way, new
5fields and methods can be added to the libraries without breaking users. This
6document will go over some patterns that can be used to test code that depends
7on the Go client libraries.
8
9## Testing gRPC services using fakes
10
11*Note*: You can see the full
12[example code using a fake here](https://github.com/googleapis/google-cloud-go/tree/main/internal/examples/fake).
13
14The clients found in `cloud.google.com/go` are gRPC based, with a couple of
15notable exceptions being the [`storage`](https://pkg.go.dev/cloud.google.com/go/storage)
16and [`bigquery`](https://pkg.go.dev/cloud.google.com/go/bigquery) clients.
17Interactions with gRPC services can be faked by serving up your own in-memory
18server within your test. One benefit of using this approach is that you don’t
19need to define an interface in your runtime code; you can keep using
20concrete struct types. You instead define a fake server in your test code. For
21example, take a look at the following function:
22
23```go
24import (
25 "context"
26 "fmt"
27 "log"
28 "os"
29
30 translate "cloud.google.com/go/translate/apiv3"
31 "github.com/googleapis/gax-go/v2"
32 translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
33)
34
35func TranslateTextWithConcreteClient(client *translate.TranslationClient, text string, targetLang string) (string, error) {
36 ctx := context.Background()
37 log.Printf("Translating %q to %q", text, targetLang)
38 req := &translatepb.TranslateTextRequest{
39 Parent: fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT")),
40 TargetLanguageCode: "en-US",
41 Contents: []string{text},
42 }
43 resp, err := client.TranslateText(ctx, req)
44 if err != nil {
45 return "", fmt.Errorf("unable to translate text: %v", err)
46 }
47 translations := resp.GetTranslations()
48 if len(translations) != 1 {
49 return "", fmt.Errorf("expected only one result, got %d", len(translations))
50 }
51 return translations[0].TranslatedText, nil
52}
53```
54
55Here is an example of what a fake server implementation would look like for
56faking the interactions above:
57
58```go
59import (
60 "context"
61
62 translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
63)
64
65type fakeTranslationServer struct {
66 translatepb.UnimplementedTranslationServiceServer
67}
68
69func (f *fakeTranslationServer) TranslateText(ctx context.Context, req *translatepb.TranslateTextRequest) (*translatepb.TranslateTextResponse, error) {
70 resp := &translatepb.TranslateTextResponse{
71 Translations: []*translatepb.Translation{
72 &translatepb.Translation{
73 TranslatedText: "Hello World",
74 },
75 },
76 }
77 return resp, nil
78}
79```
80
81All of the generated protobuf code found in [google.golang.org/genproto](https://pkg.go.dev/google.golang.org/genproto)
82contains a similar `package.UnimplementedFooServer` type that is useful for
83creating fakes. By embedding the unimplemented server in the
84`fakeTranslationServer`, the fake will “inherit” all of the RPCs the server
85exposes. Then, by providing our own `fakeTranslationServer.TranslateText`
86method you can “override” the default unimplemented behavior of the one RPC that
87you would like to be faked.
88
89The test itself does require a little bit of setup: start up a `net.Listener`,
90register the server, and tell the client library to call the server:
91
92```go
93import (
94 "context"
95 "net"
96 "testing"
97
98 translate "cloud.google.com/go/translate/apiv3"
99 "google.golang.org/api/option"
100 translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
101 "google.golang.org/grpc"
102 "google.golang.org/grpc/credentials/insecure"
103)
104
105func TestTranslateTextWithConcreteClient(t *testing.T) {
106 ctx := context.Background()
107
108 // Setup the fake server.
109 fakeTranslationServer := &fakeTranslationServer{}
110 l, err := net.Listen("tcp", "localhost:0")
111 if err != nil {
112 t.Fatal(err)
113 }
114 gsrv := grpc.NewServer()
115 translatepb.RegisterTranslationServiceServer(gsrv, fakeTranslationServer)
116 fakeServerAddr := l.Addr().String()
117 go func() {
118 if err := gsrv.Serve(l); err != nil {
119 panic(err)
120 }
121 }()
122
123 // Create a client.
124 client, err := translate.NewTranslationClient(ctx,
125 option.WithEndpoint(fakeServerAddr),
126 option.WithoutAuthentication(),
127 option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
128 )
129 if err != nil {
130 t.Fatal(err)
131 }
132
133 // Run the test.
134 text, err := TranslateTextWithConcreteClient(client, "Hola Mundo", "en-US")
135 if err != nil {
136 t.Fatal(err)
137 }
138 if text != "Hello World" {
139 t.Fatalf("got %q, want Hello World", text)
140 }
141}
142```
143
144## Testing using mocks
145
146*Note*: You can see the full
147[example code using a mock here](https://github.com/googleapis/google-cloud-go/tree/main/internal/examples/mock).
148
149When mocking code you need to work with interfaces. Let’s create an interface
150for the `cloud.google.com/go/translate/apiv3` client used in the
151`TranslateTextWithConcreteClient` function mentioned in the previous section.
152The `translate.Client` has over a dozen methods but this code only uses one of
153them. Here is an interface that satisfies the interactions of the
154`translate.Client` in this function.
155
156```go
157type TranslationClient interface {
158 TranslateText(ctx context.Context, req *translatepb.TranslateTextRequest, opts ...gax.CallOption) (*translatepb.TranslateTextResponse, error)
159}
160```
161
162Now that we have an interface that satisfies the method being used we can
163rewrite the function signature to take the interface instead of the concrete
164type.
165
166```go
167func TranslateTextWithInterfaceClient(client TranslationClient, text string, targetLang string) (string, error) {
168// ...
169}
170```
171
172This allows a real `translate.Client` to be passed to the method in production
173and for a mock implementation to be passed in during testing. This pattern can
174be applied to any Go code, not just `cloud.google.com/go`. This is because
175interfaces in Go are implicitly satisfied. Structs in the client libraries can
176implicitly implement interfaces defined in your codebase. Let’s take a look at
177what it might look like to define a lightweight mock for the `TranslationClient`
178interface.
179
180```go
181import (
182 "context"
183 "testing"
184
185 "github.com/googleapis/gax-go/v2"
186 translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
187)
188
189type mockClient struct{}
190
191func (*mockClient) TranslateText(_ context.Context, req *translatepb.TranslateTextRequest, opts ...gax.CallOption) (*translatepb.TranslateTextResponse, error) {
192 resp := &translatepb.TranslateTextResponse{
193 Translations: []*translatepb.Translation{
194 &translatepb.Translation{
195 TranslatedText: "Hello World",
196 },
197 },
198 }
199 return resp, nil
200}
201
202func TestTranslateTextWithAbstractClient(t *testing.T) {
203 client := &mockClient{}
204 text, err := TranslateTextWithInterfaceClient(client, "Hola Mundo", "en-US")
205 if err != nil {
206 t.Fatal(err)
207 }
208 if text != "Hello World" {
209 t.Fatalf("got %q, want Hello World", text)
210 }
211}
212```
213
214If you prefer to not write your own mocks there are mocking frameworks such as
215[golang/mock](https://github.com/golang/mock) which can generate mocks for you
216from an interface. As a word of caution though, try to not
217[overuse mocks](https://testing.googleblog.com/2013/05/testing-on-toilet-dont-overuse-mocks.html).
218
219## Testing using emulators
220
221Some of the client libraries provided in `cloud.google.com/go` support running
222against a service emulator. The concept is similar to that of using fakes,
223mentioned above, but the server is managed for you. You just need to start it up
224and instruct the client library to talk to the emulator by setting a service
225specific emulator environment variable. Current services/environment-variables
226are:
227
228- bigtable: `BIGTABLE_EMULATOR_HOST`
229- datastore: `DATASTORE_EMULATOR_HOST`
230- firestore: `FIRESTORE_EMULATOR_HOST`
231- pubsub: `PUBSUB_EMULATOR_HOST`
232- spanner: `SPANNER_EMULATOR_HOST`
233- storage: `STORAGE_EMULATOR_HOST`
234 - Although the storage client supports an emulator environment variable there is no official emulator provided by gcloud.
235
236For more information on emulators please refer to the
237[gcloud documentation](https://cloud.google.com/sdk/gcloud/reference/beta/emulators).
View as plain text