go-github-mock

A library to aid unittesting code that uses Golang's Github SDK

Installation

go get github.com/migueleliasweb/go-github-mock

Features

  • Create mocks for successive calls for the same endpoint
  • Mock error returns
  • High level abstraction helps writing readabe unittests (see mock.WithRequestMatch)
  • Lower level abstraction for advanced uses (see mock.WithRequestMatchHandler)

Example

import "github.com/migueleliasweb/go-github-mock/src/mock"

Multiple requests

mockedHTTPClient := mock.NewMockedHTTPClient(
    mock.WithRequestMatch(
        mock.GetUsersByUsername,
        [][]byte{
            mock.MustMarshal(github.User{
                Name: github.String("foobar"),
            }),
        },
    ),
    mock.WithRequestMatch(
        mock.GetUsersOrgsByUsername,
        [][]byte{
            mock.MustMarshal([]github.Organization{
                {
                    Name: github.String("foobar123thisorgwasmocked"),
                },
            }),
        },
    ),
    mock.WithRequestMatchHandler(
        mock.GetOrgsProjectsByOrg,
        http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
            w.Write(mock.MustMarshal([]github.Project{
                {
                    Name: github.String("mocked-proj-1"),
                },
                {
                    Name: github.String("mocked-proj-2"),
                },
            }))
        }),
    ),
)
c := github.NewClient(mockedHTTPClient)

ctx := context.Background()

user, _, userErr := c.Users.Get(ctx, "myuser")

// user.Name == "foobar"

orgs, _, orgsErr := c.Organizations.List(
    ctx,
    *(user.Name),
    nil,
)

// orgs[0].Name == "foobar123thisorgwasmocked"

projs, _, projsErr := c.Organizations.ListProjects(
    ctx,
    *orgs[0].Name,
    &github.ProjectListOptions{},
)

// projs[0].Name == "mocked-proj-1"
// projs[1].Name == "mocked-proj-2"

Mocking errors from the API

mockedHTTPClient := mock.NewMockedHTTPClient(
    mock.WithRequestMatchHandler(
        mock.GetUsersByUsername,
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            WriteError(
                w,
                http.StatusInternalServerError,
                "github went belly up or something",
            )
        }),
    ),
)
c := github.NewClient(mockedHTTPClient)

ctx := context.Background()

user, _, userErr := c.Users.Get(ctx, "someUser")

// user == nil

if userErr == nil {	
    if ghErr, ok := userErr.(*github.ErrorResponse); ok {
        fmt.Println(ghErr.Message) // == "github went belly up or something"
    }
}

Mocking with pagination

mockedHTTPClient := NewMockedHTTPClient(
    WithRequestMatchPages(
        GetOrgsReposByOrg,
        [][]byte{
            MustMarshal([]github.Repository{
                {
                    Name: github.String("repo-A-on-first-page"),
                },
                {
                    Name: github.String("repo-B-on-first-page"),
                },
            }),
            MustMarshal([]github.Repository{
                {
                    Name: github.String("repo-C-on-second-page"),
                },
                {
                    Name: github.String("repo-D-on-second-page"),
                },
            }),
        },
    ),
)

c := github.NewClient(mockedHTTPClient)

ctx := context.Background()

opt := &github.RepositoryListByOrgOptions{
    ListOptions: github.ListOptions{
        // in fact, the perPage option is ignored my the mocks
        // but this would be present in production code
        PerPage: 2,
    },
}

var allRepos []*github.Repository

for {
    repos, resp, listErr := c.Repositories.ListByOrg(ctx, "foobar", opt)

    if listErr != nil {
        t.Errorf("error listing repositories: %s", listErr.Error())
    }

    // len(repos) == 2

    allRepos = append(allRepos, repos...)

    if resp.NextPage == 0 {
        break
    }

    opt.Page = resp.NextPage
}

// matches the mock definitions (len(page[0]) + len(page[1])
// len(allRepos) == 4

Why

Some conversations got started on go-github#1800 since go-github didn't provide an interface that could be easily reimplemented for unittests. After lots of conversations from the folks from go-github and quite a few PR ideas later, this style of testing was deemed not suitable to be part of the core SDK as it's not a feature of the API itself. Nonetheless, the ability of writing unittests for code that uses the go-github package is critical.

A reuseable, and not overly verbose, way of writing the tests was reached after some more interactions (months down the line) and here we are.

GitHub

https://github.com/migueleliasweb/go-github-mock