...

Text file src/cloud.google.com/go/testing.md

Documentation: cloud.google.com/go

     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