237 lines
8.9 KiB
Markdown
237 lines
8.9 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/master/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/master/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).
|