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

go-github-mock

Go Reference Go Report Card

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"
    }
}

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.

Thanks

Thanks for all ideas and feedback from the folks in go-github.

License

This library is distributed under the MIT License found in LICENSE.

Owner
Comments
  • Fails to handle GetArchiveLink routes

    Fails to handle GetArchiveLink routes

    Using the following logic (with a custom EndpointPattern because the already existing pattern assumes you're specifying a ref) I get an Get "": unsupported protocol scheme "" response.

    mock.WithRequestMatchHandler(mock.EndpointPattern{
      Pattern: "/repos/{owner}/{repo}/zipball",
      Method:  "GET",
    }, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
      rw.WriteHeader(http.StatusFound)
      rw.Header().Add("Location", "https://codeload.github.com/gothinkster/react-redux-realworld-example-app/legacy.zip/refs/heads/master")
    })),
    

    I'm unsure if what I'm doing is inherently wrong, but I believe I'm returning the same response that Github would return with the http.StatusFound response and with a header identifying the location of the requested zipball.


    Also, let me just say, I really appreciate this project - it's gotten me 95% of the way where I need to be for a project I'm working on. So, thank you!

  • can not correctly mock GetReposContentsByOwnerByRepoByPath when path has

    can not correctly mock GetReposContentsByOwnerByRepoByPath when path has "/"

    		mock.WithRequestMatch(
    			mock.GetReposContentsByOwnerByRepoByPath,
    			github.RepositoryContent{
    				Encoding: github.String("base64"),
    				Path:     github.String("path/test-file.txt"),
    				Content:  github.String("fake-content"),
    			},
    		),
    

    if I call it with

    client.Repositories.GetContents(
    			*r.context, r.org, r.repo, github.String("path/test-file.txt"), &github.RepositoryContentGetOptions{})
    

    it will raise exception

    %!s(PANIC=Error method: runtime error: invalid memory address or nil pointer dereference)\nresponse: {{\"Response\":null,\"message\":\"mock response not found for /repos/owner/repo-name/contents/path/test-file.txt\",\"errors\":null}}"
    

    seems to be the path could not match pattern

  • Unable to mock Git.GetRef

    Unable to mock Git.GetRef

    I tried mocking Git.GetRef but unable to make it work

    mockedHTTPClient := mock.NewMockedHTTPClient(
    	mock.WithRequestMatch(
    		mock.GetReposGitRefByOwnerByRepoByRef,
    		github.Reference{},
    	),
    )
    c := github.NewClient(mockedHTTPClient)
    ctx := context.Background()
    
    reference, resp, err := c.Git.GetRef(ctx, "foo", "bar", "main")
    log.Println(reference)
    log.Println(resp.Response.StatusCode)
    log.Println(resp.Response.Request)
    
    // time="2022-03-23T12:21:53+04:00" level=info msg="<nil>"
    // time="2022-03-23T12:21:53+04:00" level=info msg=404
    // time="2022-03-23T12:21:53+04:00" level=info msg="&{GET http://127.0.0.1:55565/repos/foo/bar/git/ref/main HTTP/1.1 1 1 map[Accept:[application/vnd.github.v3+json] User-Agent:[go-github]] <nil> <nil> 0 [] false api.github.com map[] map[] <nil> map[]   <nil> <nil> <nil> 0xc00001c0b0}"
    

    I tried similar mock with c.Git.CreateRef and works fine. Anything I'm doing wrong that should be changed?

  • Question: How to mock pagination?

    Question: How to mock pagination?

    How to mock pagination?

    Use case

    This is a slightly modified version of the pagination example taken from https://github.com/google/go-github/blob/54ec837fb63dc356426c665bddb5a5b4354e621d/README.md#pagination

    func ListAllReposByOrg(ctx context.Context, org string, client *github.Client) ([]*github.Repository, error) {
    	opt := &github.RepositoryListByOrgOptions{
    		ListOptions: github.ListOptions{PerPage: 2},
    	}
    
    	var allRepos []*github.Repository
    	for {
    		repos, resp, err := client.Repositories.ListByOrg(ctx, org, opt)
    		if err != nil {
    			return nil, err
    		}
    		allRepos = append(allRepos, repos...)
    		if resp.NextPage == 0 {
    			break
    		}
    		opt.Page = resp.NextPage
    	}
    	
    	return allRepos, nil
    }
    

    Using responsesFIFO I can mock different results for subsequent requests. But, how to mock resp.NextPage?

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

    Thanks!

  • Suggestions and findings

    Suggestions and findings

    This is very useful and saved me quite some time, when I otherwise would have had to do this "by hand" when integrating Github tests into our testify based unit tests.

    Is this still being actively developed and supported?

    I've had a look at the source and noticed something that are causing issues:

    WithRequestMatch is a simple FIFO queue, which makes it hard to use the package in testify suites: without a clear, deterministic order of test invocation, you don't know which response will be encountered and whether it "matches" the test being run.

    Also, it would be nice if there were a way to retrieve mocks based on the parameter passed in, for example users/{user}/repos: I may want a different reponse for "joe" versus "thisuserdoesntexist". At first glance it wasn't obvious how to implement that without ripping the entire queue approach apart.

    As a result you have to keep the mock results within a test, which really bloats the test when you also already have test tables shoved in there.

  • Repositories.GetCommitSHA1 doesn't work as expected

    Repositories.GetCommitSHA1 doesn't work as expected

    I'm starting to use this awesome mock package, and doing some tests on the different endpoints I've notices that i can mock client.Repositories.GetCommitSHA1 using the GetReposCommitsByOwnerByRepoByRef. At first it worked great, but once i've started to send more complex ref it fell apart. for example

    sha, resp, err := ghClient.Repositories.GetCommitSHA1(context.Background(), "gg", "mocked-repo-2", "refs/heads/ddd", "") panics out. this is due to the /heads/ddd is added to the endpoint, and so i'm guessing is not get picked up by the mock implementation.... currently i've manually added

    mockSha := mock.EndpointPattern{
      Pattern: "/repos/{owner}/{repo}/commits/{ref}/heads/{x}",
      Method:  "GET",
    }
    

    but it could be nice if the mock package can solve this

  • Support for empty result

    Support for empty result

    This PR adds support for the cases when empty data should be returned (for example if no releases exists - Github returns http 200 with empty array in this case )

  • Question: how to sponsor this project?

    Question: how to sponsor this project?

    Hi 👋 ,

    Thanks so much for the work on this project! At @Reviewpad, we are trying to support the OSS projects that we use as dependencies to our main repository reviewpad/reviewpad.

    Do you plan to join any sponsoring program? We are also happy to help the community to maintain the project.

    Thanks!

  • mock.GetRateLimits returns nothing

    mock.GetRateLimits returns nothing

    I am trying to mock the request which should return an amount of remaining requests

    mockedHTTPClient := mock.NewMockedHTTPClient(
    	mock.WithRequestMatch(
    		mock.GetRateLimit,
    		github.RateLimits{
    			Core: &github.Rate{
    				Limit:     *github.Int(1),
    				Remaining: 1,
    				Reset:     github.Timestamp{},
    			},
    			Search: &github.Rate{},
    		},
    	),
    )
    c := github.NewClient(mockedHTTPClient)
    ctx := context.Background()
    
    limits, response, err := c.RateLimits(ctx)
    
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Println(response, limits, err)
    

    For some reason my output looks like this

    github.Rate{Limit:0, Remaining:0, Reset:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}} <nil> <nil>
    

    I was trying to return just a github.Rate, github.RateLimits. I've swapped values with github.Int. Still nothing. Use of WithRequestMatchHandler also did nothing. Am I missing something? Other requests I was trying to mock are working as expected and returns proper values.

  • Mock test for function that calls the same api request twice

    Mock test for function that calls the same api request twice

    I am having some errors when I created two MockBackendOption with PatchReposPullsByOwnerByRepoByPullNumber for the function NewMockedHTTPClient, its an EOF error , I don't know if it is an error of my code or go-GitHub-mock , if it is my error what should I do instead of that. I am trying to create a function that calls the same GitHub API request twice.

  • Dynamically create routes without optional parameters

    Dynamically create routes without optional parameters

    Some endpoints have parameters that are optional, we should be able to dynamically create endpoints without them to cater to some other code flows.

    E.g.

    "/repos/{owner}/{repo}/zipball/{ref}": {
          "get": {
            "summary": "Download a repository archive (zip)",
            "description": "Gets a redirect URL to download a zip archive for a repository. If you omit `:ref`, the repository’s default branch (usually\n`master`) will be used. Please make sure your HTTP framework is configured to follow redirects or you will need to use\nthe `Location` header to make a second `GET` request.\n**Note**: For private repositories, these links are temporary and expire after five minutes.",
            "tags": [
              "repos"
            ],
            "externalDocs": {
              "description": "API method documentation",
              "url": "https://docs.github.com/rest/reference/repos#download-a-repository-archive"
            },
            "operationId": "repos/download-zipball-archive",
            "parameters": [
              {
                "$ref": "#/components/parameters/owner"
              },
              {
                "$ref": "#/components/parameters/repo"
              },
              {
                "name": "ref",
                "in": "path",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            ],
            "responses": {
              "302": {
                "description": "Response",
                "headers": {
                  "Location": {
                    "example": "https://codeload.github.com/me/myprivate/legacy.zip/master?login=me&token=thistokenexpires",
                    "schema": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "x-github": {
              "githubCloudOnly": false,
              "enabledForGitHubApps": true,
              "category": "repos",
              "subcategory": "contents"
            }
          }
        },
    

    The above example shows that only owner and repo are required arguments.

  • The `github.ErrorResponse.Error()` method panics

    The `github.ErrorResponse.Error()` method panics

    The ErrorResponse.Error() implementation references the *http.Response which is not provided by go-github-mock. The examples use type assertions to work around this, but wrapping errors is a common pattern in Go (e.g. fmt.Errorf("failed: %w", userErr)). If you're testing code which wraps the error, it will panic.

    diff --git a/src/mock/server_test.go b/src/mock/server_test.go
    index c88b41f..25e0d58 100644
    --- a/src/mock/server_test.go
    +++ b/src/mock/server_test.go
    @@ -118,6 +118,7 @@ func TestMockErrors(t *testing.T) {
                    t.Fatal("user err is nil, want *github.ErrorResponse")
            }
    
    +       t.Errorf("%s", userErr.Error())
            ghErr, ok := userErr.(*github.ErrorResponse)
    
            if !ok {
    
    ❯ go test ./...
    ?   	github.com/migueleliasweb/go-github-mock	[no test files]
    --- FAIL: TestMockErrors (0.00s)
    panic: runtime error: invalid memory address or nil pointer dereference [recovered]
    	panic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV: segmentation violation code=0x1 addr=0x80 pc=0x12e6a1a]
    
    goroutine 50 [running]:
    testing.tRunner.func1.2(0x1334920, 0x15b67d0)
    	.../golang/1.16.3/go/src/testing/testing.go:1143 +0x332
    testing.tRunner.func1(0xc0002b2000)
    	.../golang/1.16.3/go/src/testing/testing.go:1146 +0x4b6
    panic(0x1334920, 0x15b67d0)
    	.../golang/1.16.3/go/src/runtime/panic.go:965 +0x1b9
    github.com/google/go-github/v37/github.(*ErrorResponse).Error(0xc00007c5f0, 0x0, 0x16)
    	.../go/pkg/mod/github.com/google/go-github/[email protected]/github/github.go:707 +0x3a
    github.com/migueleliasweb/go-github-mock/src/mock.TestMockErrors(0xc0002b2000)
    	.../go-github-mock/src/mock/server_test.go:121 +0x1e3
    testing.tRunner(0xc0002b2000, 0x13b3ef8)
    	.../golang/1.16.3/go/src/testing/testing.go:1193 +0xef
    created by testing.(*T).Run
    	.../golang/1.16.3/go/src/testing/testing.go:1238 +0x2b3
    FAIL	github.com/migueleliasweb/go-github-mock/src/mock	0.133s
    FAIL
    
Test your code without writing mocks with ephemeral Docker containers 📦 Setup popular services with just a couple lines of code ⏱️ No bash, no yaml, only code 💻

Gnomock – tests without mocks ??️ Spin up entire dependency stack ?? Setup initial dependency state – easily! ?? Test against actual, close to product

Dec 29, 2022
Just Dance Unlimited mock-up server written on Golang and uses a popular Gin framework for Go.

BDCS Just Dance Unlimited mock-up server written on Golang and uses a popular Gin framework for Go. Features Security Authorization works using UbiSer

Nov 10, 2021
Terratest is a Go library that makes it easier to write automated tests for your infrastructure code.

Terratest is a Go library that makes it easier to write automated tests for your infrastructure code. It provides a variety of helper functions and patterns for common infrastructure testing tasks,

Dec 30, 2022
:octocat: ghdag is a tiny workflow engine for GitHub issue and pull request.

ghdag ghdag is a tiny workflow engine for GitHub issue and pull request. Key features of ghdag are: Simple definition of workflows to improve the life

Nov 5, 2022
go-wrk - a HTTP benchmarking tool based in spirit on the excellent wrk tool (https://github.com/wg/wrk)

go-wrk - an HTTP benchmarking tool go-wrk is a modern HTTP benchmarking tool capable of generating significant load when run on a single multi-core CP

Jan 5, 2023
Example usage of github.com/go-webauthn/webauthn

Example This is an example go + React application which shows off the functionality of the github.com/go-webauthn/webauthn library. Features This proj

Sep 26, 2022
Mutation testing for Go source code

go-mutesting go-mutesting is a framework for performing mutation testing on Go source code. Its main purpose is to find source code, which is not cove

Dec 28, 2022
mockery - A mock code autogenerator for Golang
mockery - A mock code autogenerator for Golang

mockery - A mock code autogenerator for Golang

Jan 8, 2023
A Master list of Go Programming Tutorials, their write-ups, their source code and their current build status!
A Master list of Go Programming Tutorials, their write-ups, their source code and their current build status!

TutorialEdge TutorialEdge.net Go Tutorials ??‍?? ??‍?? Welcome to the TutorialEdge Go Repository! The goal of this repo is to be able to keep track of

Dec 18, 2022
Automatically generate Go test boilerplate from your source code.
Automatically generate Go test boilerplate from your source code.

gotests gotests makes writing Go tests easy. It's a Golang commandline tool that generates table driven tests based on its target source files' functi

Jan 8, 2023
Demo repository for Infrastructure as Code testing tools and frameworks.

Testing Infrastructure as Code Demo repository for Infrastructure as Code testing tools and frameworks. Maintainer M.-Leander Reimer (@lreimer), mario

Jan 23, 2022
Sample code for a quick demo of go 1.18's fuzzing

Fuzzing in Go 1.18 What is it? "Fuzzing is a type of automated testing which continuously manipulates inputs to a program to find bugs. Go fuzzing use

Feb 11, 2022
Fortio load testing library, command line tool, advanced echo server and web UI in go (golang). Allows to specify a set query-per-second load and record latency histograms and other useful stats.
Fortio load testing library, command line tool, advanced echo server and web UI in go (golang). Allows to specify a set query-per-second load and record latency histograms and other useful stats.

Fortio Fortio (Φορτίο) started as, and is, Istio's load testing tool and now graduated to be its own project. Fortio is also used by, among others, Me

Jan 2, 2023
:exclamation:Basic Assertion Library used along side native go testing, with building blocks for custom assertions

Package assert Package assert is a Basic Assertion library used along side native go testing Installation Use go get. go get github.com/go-playground/

Jan 6, 2023
Library created for testing JSON against patterns.

Gomatch Library created for testing JSON against patterns. The goal was to be able to validate JSON focusing only on parts essential in given test cas

Oct 28, 2022
A BDD library for Go

gospecify This provides a BDD syntax for testing your Go code. It should be familiar to anybody who has used libraries such as rspec. Installation The

Sep 27, 2022
A Go test assertion library for verifying that two representations of JSON are semantically equal
A Go test assertion library for verifying that two representations of JSON are semantically equal

jsonassert is a Go test assertion library for verifying that two representations of JSON are semantically equal. Usage Create a new *jsonassert.Assert

Jan 4, 2023
A Go library help testing your RESTful API application

RESTit A Go micro-framework to help writing RESTful API integration test Package RESTit provides helps to those who want to write an integration test

Oct 28, 2022
testcase is an opinionated behavior-driven-testing library

Table of Contents testcase Guide Official API Documentation Getting Started / Example Modules Summary DRY Modularization Stability Case Study About te

Nov 10, 2022