Mocking with Go

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 :slight_smile:

Hi, @drpaneas, It looks like the Get function is on a Repositories field in the *github.Client, not directly on the client. You should be able to change your GetUserRepo function to this:

type Repositories interface {
	Get (ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error)
}

func GetUserRepo(repos Repositories, user string) (*github.Repository, error) {
    repo, _, err := repos.Get(context.Background(), user, "ghostfish")
    if err != nil {
        return nil, err
    }
    return repo, err
}

And then use it like:

repo, err := GetUserRepo(client.Repositories, "drpaneas")

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.