The client libraries generated as a part of google.golang.org/api
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 these libraries.
Note: You can see the full example code using a fake here.
The services found in google.golang.org/api
are all HTTP based.
Interactions with HTTP 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 the
concrete struct types returned by the client library. For example, take a look
at the following function:
import (
"fmt"
"os"
"google.golang.org/api/translate/v3"
)
// TranslateText translates text to the given language using the provided
// service.
func TranslateText(service *translate.Service, text, language string) (string, error) {
parent := fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT"))
req := &translate.TranslateTextRequest{
TargetLanguageCode: language,
Contents: []string{text},
}
resp, err := service.Projects.Locations.TranslateText(parent, req).Do()
if err != nil {
return "", fmt.Errorf("unable to translate text: %v", err)
}
return resp.Translations[0].TranslatedText, nil
}
To fake HTTP interactions we can make use of the httptest
package found in the
standard library. The server URL obtained from creating the test server can be
passed to the service constructor with the use of option.WithEndpoint
. This
instructs the service to route all traffic to your test sever rather than the
live service. Here is an example of what doing this looks like when calling the
above function:
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"google.golang.org/api/option"
"google.golang.org/api/translate/v3"
)
func TestTranslateText(t *testing.T) {
ctx := context.Background()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := &translate.TranslateTextResponse{
Translations: []*translate.Translation{
{TranslatedText: "Hello World"},
},
}
b, err := json.Marshal(resp)
if err != nil {
http.Error(w, "unable to marshal request: "+err.Error(), http.StatusBadRequest)
return
}
w.Write(b)
}))
defer ts.Close()
svc, err := translate.NewService(ctx, option.WithoutAuthentication(), option.WithEndpoint(ts.URL))
if err != nil {
t.Fatalf("unable to create client: %v", err)
}
text, err := TranslateText(svc, "Hola Mundo", "en-US")
if err != nil {
t.Fatal(err)
}
if text != "Hello World" {
t.Fatalf("got %q, want Hello World", text)
}
}
Note: You can see the full example code using a mocks here.
When mocking code you need to work with interfaces. Because the services in
google.golang.org/api
use the builder pattern to construct and execute
requests it can be tedious to create low-level interfaces that match methods
found on the services directly. Although this can be done and you can find
examples of this in full code linked above. Another approach that will keep your
interfaces cleaner is to create a high-level interface that accepts all of the
required input and returns the desired outputs. This hides the complexity of the
services builder pattern. This concept is sometimes referred to as the facade
pattern. Here is an example of a high level interface for the TranslateText
method:
// TranslateService is a facade of a `translate.Service`, specifically used to
// for translating text.
type TranslateService interface {
TranslateText(text, language string) (string, error)
}
// TranslateTextHighLevel translates text to the given language using the
// provided service.
func TranslateTextHighLevel(service TranslateService, text, language string) (string, error) {
return service.TranslateText(text, language)
}
This interface allows a concrete translate.Service
to be wrapped and passed to
the function in production and for a mock implementation to be passed in during
testing. Here is what it would look like to create a wrapper around a translate.Service
to fullfil the TranslateService
interface:
import (
"context"
"fmt"
"log"
"os"
"google.golang.org/api/option"
"google.golang.org/api/translate/v3"
)
type translateService struct {
svc *translate.Service
}
// NewTranslateService creates a TranslateService.
func NewTranslateService(ctx context.Context, opts ...option.ClientOption) TranslateService {
svc, err := translate.NewService(ctx, opts...)
if err != nil {
log.Fatalf("unable to create translate service, shutting down: %v", err)
}
return &translateService{svc}
}
func (t *translateService) TranslateText(text, language string) (string, error) {
parent := fmt.Sprintf("projects/%s/locations/global", os.Getenv("GOOGLE_CLOUD_PROJECT"))
resp, err := t.svc.Projects.Locations.TranslateText(parent, &translate.TranslateTextRequest{
TargetLanguageCode: language,
Contents: []string{text},
}).Do()
if err != nil {
return "", fmt.Errorf("unable to translate text: %v", err)
}
return resp.Translations[0].TranslatedText, nil
}
Let’s take a look at what it might look like to define a lightweight mock for
the TranslateService
interface.
import "testing"
// mockService fulfills the TranslateService interface.
type mockService struct{}
func (*mockService) TranslateText(text, language string) (string, error) {
return "Hello World", nil
}
func TestTranslateTextHighLevel(t *testing.T) {
svc := &mockService{}
text, err := TranslateTextHighLevel(svc, "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 which can generate mocks for you from an interface. As a word of caution though, try to not overuse mocks.