Marvin Preuss
d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
237 lines
8.8 KiB
Markdown
237 lines
8.8 KiB
Markdown
# Testing Code that depends on Go Client Libraries
|
||
|
||
The Go client libraries generated as a part of `cloud.google.com/go` all take
|
||
the approach of returning concrete types instead of interfaces. That way, new
|
||
fields and methods can be added to the libraries without breaking users. This
|
||
document will go over some patterns that can be used to test code that depends
|
||
on the Go client libraries.
|
||
|
||
## Testing gRPC services using fakes
|
||
|
||
*Note*: You can see the full
|
||
[example code using a fake here](https://github.com/googleapis/google-cloud-go/tree/main/internal/examples/fake).
|
||
|
||
The clients found in `cloud.google.com/go` are gRPC based, with a couple of
|
||
notable exceptions being the [`storage`](https://pkg.go.dev/cloud.google.com/go/storage)
|
||
and [`bigquery`](https://pkg.go.dev/cloud.google.com/go/bigquery) clients.
|
||
Interactions with gRPC services can be faked by serving up your own in-memory
|
||
server within your test. One benefit of using this approach is that you don’t
|
||
need to define an interface in your runtime code; you can keep using
|
||
concrete struct types. You instead define a fake server in your test code. For
|
||
example, take a look at the following function:
|
||
|
||
```go
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
|
||
translate "cloud.google.com/go/translate/apiv3"
|
||
"github.com/googleapis/gax-go/v2"
|
||
translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
|
||
)
|
||
|
||
func TranslateTextWithConcreteClient(client *translate.TranslationClient, text string, targetLang string) (string, error) {
|
||
ctx := context.Background()
|
||
log.Printf("Translating %q to %q", text, targetLang)
|
||
req := &translatepb.TranslateTextRequest{
|
||
Parent: fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT")),
|
||
TargetLanguageCode: "en-US",
|
||
Contents: []string{text},
|
||
}
|
||
resp, err := client.TranslateText(ctx, req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("unable to translate text: %v", err)
|
||
}
|
||
translations := resp.GetTranslations()
|
||
if len(translations) != 1 {
|
||
return "", fmt.Errorf("expected only one result, got %d", len(translations))
|
||
}
|
||
return translations[0].TranslatedText, nil
|
||
}
|
||
```
|
||
|
||
Here is an example of what a fake server implementation would look like for
|
||
faking the interactions above:
|
||
|
||
```go
|
||
import (
|
||
"context"
|
||
|
||
translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
|
||
)
|
||
|
||
type fakeTranslationServer struct {
|
||
translatepb.UnimplementedTranslationServiceServer
|
||
}
|
||
|
||
func (f *fakeTranslationServer) TranslateText(ctx context.Context, req *translatepb.TranslateTextRequest) (*translatepb.TranslateTextResponse, error) {
|
||
resp := &translatepb.TranslateTextResponse{
|
||
Translations: []*translatepb.Translation{
|
||
&translatepb.Translation{
|
||
TranslatedText: "Hello World",
|
||
},
|
||
},
|
||
}
|
||
return resp, nil
|
||
}
|
||
```
|
||
|
||
All of the generated protobuf code found in [google.golang.org/genproto](https://pkg.go.dev/google.golang.org/genproto)
|
||
contains a similar `package.UnimplmentedFooServer` type that is useful for
|
||
creating fakes. By embedding the unimplemented server in the
|
||
`fakeTranslationServer`, the fake will “inherit” all of the RPCs the server
|
||
exposes. Then, by providing our own `fakeTranslationServer.TranslateText`
|
||
method you can “override” the default unimplemented behavior of the one RPC that
|
||
you would like to be faked.
|
||
|
||
The test itself does require a little bit of setup: start up a `net.Listener`,
|
||
register the server, and tell the client library to call the server:
|
||
|
||
```go
|
||
import (
|
||
"context"
|
||
"net"
|
||
"testing"
|
||
|
||
translate "cloud.google.com/go/translate/apiv3"
|
||
"google.golang.org/api/option"
|
||
translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
|
||
"google.golang.org/grpc"
|
||
)
|
||
|
||
func TestTranslateTextWithConcreteClient(t *testing.T) {
|
||
ctx := context.Background()
|
||
|
||
// Setup the fake server.
|
||
fakeTranslationServer := &fakeTranslationServer{}
|
||
l, err := net.Listen("tcp", "localhost:0")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
gsrv := grpc.NewServer()
|
||
translatepb.RegisterTranslationServiceServer(gsrv, fakeTranslationServer)
|
||
fakeServerAddr := l.Addr().String()
|
||
go func() {
|
||
if err := gsrv.Serve(l); err != nil {
|
||
panic(err)
|
||
}
|
||
}()
|
||
|
||
// Create a client.
|
||
client, err := translate.NewTranslationClient(ctx,
|
||
option.WithEndpoint(fakeServerAddr),
|
||
option.WithoutAuthentication(),
|
||
option.WithGRPCDialOption(grpc.WithInsecure()),
|
||
)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Run the test.
|
||
text, err := TranslateTextWithConcreteClient(client, "Hola Mundo", "en-US")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if text != "Hello World" {
|
||
t.Fatalf("got %q, want Hello World", text)
|
||
}
|
||
}
|
||
```
|
||
|
||
## Testing using mocks
|
||
|
||
*Note*: You can see the full
|
||
[example code using a mock here](https://github.com/googleapis/google-cloud-go/tree/main/internal/examples/mock).
|
||
|
||
When mocking code you need to work with interfaces. Let’s create an interface
|
||
for the `cloud.google.com/go/translate/apiv3` client used in the
|
||
`TranslateTextWithConcreteClient` function mentioned in the previous section.
|
||
The `translate.Client` has over a dozen methods but this code only uses one of
|
||
them. Here is an interface that satisfies the interactions of the
|
||
`translate.Client` in this function.
|
||
|
||
```go
|
||
type TranslationClient interface {
|
||
TranslateText(ctx context.Context, req *translatepb.TranslateTextRequest, opts ...gax.CallOption) (*translatepb.TranslateTextResponse, error)
|
||
}
|
||
```
|
||
|
||
Now that we have an interface that satisfies the method being used we can
|
||
rewrite the function signature to take the interface instead of the concrete
|
||
type.
|
||
|
||
```go
|
||
func TranslateTextWithInterfaceClient(client TranslationClient, text string, targetLang string) (string, error) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
This allows a real `translate.Client` to be passed to the method in production
|
||
and for a mock implementation to be passed in during testing. This pattern can
|
||
be applied to any Go code, not just `cloud.google.com/go`. This is because
|
||
interfaces in Go are implicitly satisfied. Structs in the client libraries can
|
||
implicitly implement interfaces defined in your codebase. Let’s take a look at
|
||
what it might look like to define a lightweight mock for the `TranslationClient`
|
||
interface.
|
||
|
||
```go
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/googleapis/gax-go/v2"
|
||
translatepb "google.golang.org/genproto/googleapis/cloud/translate/v3"
|
||
)
|
||
|
||
type mockClient struct{}
|
||
|
||
func (*mockClient) TranslateText(_ context.Context, req *translatepb.TranslateTextRequest, opts ...gax.CallOption) (*translatepb.TranslateTextResponse, error) {
|
||
resp := &translatepb.TranslateTextResponse{
|
||
Translations: []*translatepb.Translation{
|
||
&translatepb.Translation{
|
||
TranslatedText: "Hello World",
|
||
},
|
||
},
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func TestTranslateTextWithAbstractClient(t *testing.T) {
|
||
client := &mockClient{}
|
||
text, err := TranslateTextWithInterfaceClient(client, "Hola Mundo", "en-US")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if text != "Hello World" {
|
||
t.Fatalf("got %q, want Hello World", text)
|
||
}
|
||
}
|
||
```
|
||
|
||
If you prefer to not write your own mocks there are mocking frameworks such as
|
||
[golang/mock](https://github.com/golang/mock) which can generate mocks for you
|
||
from an interface. As a word of caution though, try to not
|
||
[overuse mocks](https://testing.googleblog.com/2013/05/testing-on-toilet-dont-overuse-mocks.html).
|
||
|
||
## Testing using emulators
|
||
|
||
Some of the client libraries provided in `cloud.google.com/go` support running
|
||
against a service emulator. The concept is similar to that of using fakes,
|
||
mentioned above, but the server is managed for you. You just need to start it up
|
||
and instruct the client library to talk to the emulator by setting a service
|
||
specific emulator environment variable. Current services/environment-variables
|
||
are:
|
||
|
||
- bigtable: `BIGTABLE_EMULATOR_HOST`
|
||
- datastore: `DATASTORE_EMULATOR_HOST`
|
||
- firestore: `FIRESTORE_EMULATOR_HOST`
|
||
- pubsub: `PUBSUB_EMULATOR_HOST`
|
||
- spanner: `SPANNER_EMULATOR_HOST`
|
||
- storage: `STORAGE_EMULATOR_HOST`
|
||
- Although the storage client supports an emulator environment variable there is no official emulator provided by gcloud.
|
||
|
||
For more information on emulators please refer to the
|
||
[gcloud documentation](https://cloud.google.com/sdk/gcloud/reference/beta/emulators).
|