I am trying to learn how to do mocking in Go, but … I think I need some help. Here’s what I have accomplished so far:
I will write a client that authenticates myself with Github API and then fetches the description of my “ghostfish” repository.
package main
import (
"context"
"fmt"
"github.com/google/go-github/v30/github"
"golang.org/x/oauth2"
"log"
"os"
)
func main() {
// Import your GitHub token
token := oauth2.Token{
AccessToken: os.Getenv("GHTOKEN"),
}
tokenSource := oauth2.StaticTokenSource(&token)
// Construct oAuth2 client with GitHub token
tc := oauth2.NewClient(context.Background(), tokenSource)
// Construct a GitHub client passing the oAuth2 client
client := github.NewClient(tc)
// Fetch information about the https://github.com/drpaneas/ghostfish repository
repo, _, err := client.Repositories.Get(context.Background(), "drpaneas", "ghostfish")
if err != nil {
log.Fatal(err)
}
// Print the description of the repository
fmt.Println(repo.GetDescription())
}
So far, nothing is being mocked. To run the code above you will need a pair of GitHub’s Token loaded into an environment variable:
$ export GHTOKEN="your token"
Running it:
$ go build
$ ./gh-client
A TCP Scanner written in Go
Separation of concerns
Having everything into a single main()
function is quite hard to write good unit-tests. A call to main()
in the test will do multiple network requests to GitHub’s API, which is not what we intend to do. Let’s break the code apart:
package main
import (
"context"
"fmt"
"github.com/google/go-github/v30/github"
"golang.org/x/oauth2"
"log"
"os"
)
func main() {
client := NewGithubClient()
repo, err := GetUserRepo(client, "drpaneas")
if err != nil {
fmt.Println("Error")
log.Fatal(err)
}
fmt.Println(repo.GetDescription())
}
func NewGithubClient() *github.Client {
token := oauth2.Token{
AccessToken: os.Getenv("GHTOKEN"),
}
tokenSource := oauth2.StaticTokenSource(&token)
tc := oauth2.NewClient(context.Background(), tokenSource)
client := github.NewClient(tc)
return client
}
func GetUserRepo(client *github.Client, user string) (*github.Repository, error) {
// repo, _, err := client.Repositories.List(context.Background(), user, nil)
repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")
if err != nil {
return nil, err
}
return repo, err
}
Better! Two distinct functions were defined:
-
NewGithubClient()
authenticates and returns a client to be used in subsequent operations, like getting the list of repositories. -
GetUserRepo()
gets the user’s repository
Notice: The GetUserRepo()
accepts a *github.Client
to do the GitHub request, this is the beginning of the dependency injection pattern. The client is being injected into the function that will use it. Could we inject a mock one? Before answering this question, take a look at this simple test:
// main_test.go
package main_test
import (
. "github.com/drpaneas/gh-client"
"os"
"testing"
)
func TestGetUserRepos(t *testing.T) {
os.Setenv("GHTOKEN", "fake token")
client := NewGithubClient()
repo, err := GetUserRepo(client, "whatever")
if err != nil {
t.Errorf("Expected nil, got %s", err)
}
if repo.GetDescription() != "A TCP Scanner written in Go" {
t.Errorf("extected 'A TCP Scanner written in Go', got %s", repo.GetDescription())
}
}
Run it:
$ go test
--- FAIL: TestGetUserRepos (1.77s)
main_test.go:14: Expected nil, got GET https://api.github.com/repos/whatever/ghostfish: 401 Bad credentials []
main_test.go:18: extected 'A TCP Scanner written in Go', got ''
FAIL
exit status 1
FAIL github.com/drpaneas/gh-client 3.896s
The program returns an error, trying to authenticate with our fake GHTOKEN
. We don’t want that.
Interfaces to the rescue
Imagine we could create a mock for *github.Client
with the same Repositories.Get()
method and signature and inject it into the GetUserRepo()
method during the test. Go is statically typed language and with the current implementation only *github.Client
can be passed into GetUserRepo()
. Thus, it needs to be refactored to accept any type with a Repositories.Get()
, like a mock client.
Fortunately, Go has the concept of interface
which is a type with a set of method signatures. If any type implements those methods, it satisfies the interface and be recognized by the interface’s type.
but… I can’t come up with the correct interface as it always complains. Here’s what I have (which doesn’t even compile):
type GithubClient interface {
Get (ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error)
}
// error 1: Cannot use 'client' (type *github.Client) as type *GithubClient
repo, err := GetUserRepo(client, "drpaneas")
// error 2: Unresolved reference 'Repositories'
repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")
Any help is much appreciated. Thanks in advance