...

Text file src/google.golang.org/api/testing.md

Documentation: google.golang.org/api

     1# Testing Code that depends on google.golang.org/api
     2
     3The client libraries generated as a part of `google.golang.org/api` 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 these libraries.
     8
     9## Testing HTTP services using fakes
    10
    11*Note*: You can see the full
    12[example code using a fake here](https://github.com/googleapis/google-api-go-client/tree/main/internal/examples/fake).
    13
    14The services found in `google.golang.org/api` are all HTTP based.
    15Interactions with HTTP services can be faked by serving up your own in-memory
    16server within your test. One benefit of using this approach is that you don’t
    17need to define an interface in your runtime code; you can keep using the
    18concrete struct types returned by the client library. For example, take a look
    19at the following function:
    20
    21```go
    22import (
    23    "fmt"
    24    "os"
    25
    26    "google.golang.org/api/translate/v3"
    27)
    28
    29// TranslateText translates text to the given language using the provided
    30// service.
    31func TranslateText(service *translate.Service, text, language string) (string, error) {
    32    parent := fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT"))
    33    req := &translate.TranslateTextRequest{
    34        TargetLanguageCode: language,
    35        Contents:           []string{text},
    36    }
    37    resp, err := service.Projects.Locations.TranslateText(parent, req).Do()
    38    if err != nil {
    39        return "", fmt.Errorf("unable to translate text: %v", err)
    40    }
    41    return resp.Translations[0].TranslatedText, nil
    42}
    43```
    44
    45To fake HTTP interactions we can make use of the `httptest` package found in the
    46standard library. The server URL obtained from creating the test server can be
    47passed to the service constructor with the use of `option.WithEndpoint`. This
    48instructs the service to route all traffic to your test sever rather than the
    49live service. Here is an example of what doing this looks like when calling the
    50above function:
    51
    52```go
    53import (
    54    "context"
    55    "encoding/json"
    56    "net/http"
    57    "net/http/httptest"
    58    "testing"
    59
    60    "google.golang.org/api/option"
    61    "google.golang.org/api/translate/v3"
    62)
    63
    64func TestTranslateText(t *testing.T) {
    65    ctx := context.Background()
    66    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    67        resp := &translate.TranslateTextResponse{
    68            Translations: []*translate.Translation{
    69                {TranslatedText: "Hello World"},
    70            },
    71        }
    72        b, err := json.Marshal(resp)
    73        if err != nil {
    74            http.Error(w, "unable to marshal request: "+err.Error(), http.StatusBadRequest)
    75            return
    76        }
    77        w.Write(b)
    78    }))
    79    defer ts.Close()
    80    svc, err := translate.NewService(ctx, option.WithoutAuthentication(), option.WithEndpoint(ts.URL))
    81    if err != nil {
    82        t.Fatalf("unable to create client: %v", err)
    83    }
    84    text, err := TranslateText(svc, "Hola Mundo", "en-US")
    85    if err != nil {
    86        t.Fatal(err)
    87    }
    88    if text != "Hello World" {
    89        t.Fatalf("got %q, want Hello World", text)
    90    }
    91}
    92```
    93
    94## Testing using mocks
    95
    96*Note*: You can see the full
    97[example code using a mocks here](https://github.com/googleapis/google-api-go-client/tree/main/internal/examples/mock).
    98
    99When mocking code you need to work with interfaces. Because the services in
   100`google.golang.org/api` use the builder pattern to construct and execute
   101requests it can be tedious to create low-level interfaces that match methods
   102found on the services directly. Although this can be done and you can find
   103examples of this in full code linked above. Another approach that will keep your
   104interfaces cleaner is to create a high-level interface that accepts all of the
   105required input and returns the desired outputs. This hides the complexity of the
   106services builder pattern. This concept is sometimes referred to as the facade
   107pattern. Here is an example of a high level interface for the `TranslateText`
   108method:
   109
   110```go
   111// TranslateService is a facade of a `translate.Service`, specifically used to
   112// for translating text.
   113type TranslateService interface {
   114    TranslateText(text, language string) (string, error)
   115}
   116
   117// TranslateTextHighLevel translates text to the given language using the
   118// provided service.
   119func TranslateTextHighLevel(service TranslateService, text, language string) (string, error) {
   120    return service.TranslateText(text, language)
   121}
   122```
   123
   124This interface allows a concrete `translate.Service` to be wrapped and passed to
   125the function in production and for a mock implementation to be passed in during
   126testing. Here is what it would look like to create a wrapper around a `translate.Service`
   127to fullfil the `TranslateService` interface:
   128
   129```go
   130import (
   131    "context"
   132    "fmt"
   133    "log"
   134    "os"
   135
   136    "google.golang.org/api/option"
   137    "google.golang.org/api/translate/v3"
   138)
   139
   140type translateService struct {
   141    svc *translate.Service
   142}
   143
   144// NewTranslateService creates a TranslateService.
   145func NewTranslateService(ctx context.Context, opts ...option.ClientOption) TranslateService {
   146    svc, err := translate.NewService(ctx, opts...)
   147    if err != nil {
   148        log.Fatalf("unable to create translate service, shutting down: %v", err)
   149    }
   150    return &translateService{svc}
   151}
   152
   153func (t *translateService) TranslateText(text, language string) (string, error) {
   154    parent := fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT"))
   155    resp, err := t.svc.Projects.Locations.TranslateText(parent, &translate.TranslateTextRequest{
   156        TargetLanguageCode: language,
   157        Contents:           []string{text},
   158    }).Do()
   159    if err != nil {
   160        return "", fmt.Errorf("unable to translate text: %v", err)
   161    }
   162    return resp.Translations[0].TranslatedText, nil
   163}
   164```
   165
   166Let’s take a look at what it might look like to define a lightweight mock for
   167the `TranslateService` interface.
   168
   169```go
   170import "testing"
   171
   172// mockService fulfills the TranslateService interface.
   173type mockService struct{}
   174
   175func (*mockService) TranslateText(text, language string) (string, error) {
   176    return "Hello World", nil
   177}
   178func TestTranslateTextHighLevel(t *testing.T) {
   179    svc := &mockService{}
   180    text, err := TranslateTextHighLevel(svc, "Hola Mundo", "en-US")
   181    if err != nil {
   182        t.Fatal(err)
   183    }
   184    if text != "Hello World" {
   185        t.Fatalf("got %q, want Hello World", text)
   186    }
   187}
   188```
   189
   190If you prefer to not write your own mocks there are mocking frameworks such as
   191[golang/mock](https://github.com/golang/mock) which can generate mocks for you
   192from an interface. As a word of caution though, try to not
   193[overuse mocks](https://testing.googleblog.com/2013/05/testing-on-toilet-dont-overuse-mocks.html).

View as plain text