如何在Golang测试中Stub go-github调用避免请求真实GitHub服务
基于go-github的接口测试Mock方案
测试GetClient函数
GetClient本身仅负责构造GitHub客户端实例,不会主动发起网络请求,无需额外打桩,只要覆盖正常、异常参数分支即可:
import "github.com/stretchr/testify/assert" func TestGetClient(t *testing.T) { // 测试异常分支:非法URL格式 t.Run("invalid url", func(t *testing.T) { client, ctx, err := GetClient("test_token", "://invalid-url") assert.Error(t, err) assert.Nil(t, client) assert.Nil(t, ctx) }) // 测试正常分支:参数正确返回合法客户端 t.Run("valid params", func(t *testing.T) { client, ctx, err := GetClient("test_token", "https://github.example.com/api/v3/") assert.NoError(t, err) assert.NotNil(t, client) assert.NotNil(t, ctx) // 可选校验客户端基础配置是否符合预期 assert.Equal(t, "https://github.example.com/api/v3/", client.BaseURL.String()) }) }
测试GetPRComments函数
该函数会调用GitHub接口,提供两种Mock方案,可根据业务场景选择:
方案1:替换HTTP传输层(零业务代码改动)
go-github底层基于标准库http.Client实现请求,我们可以自定义HTTP传输层返回预设响应,完全规避真实网络请求:
import ( "io" "net/http" "strings" "github.com/google/go-github/v55/github" "github.com/stretchr/testify/assert" ) // MockRoundTripper 自定义HTTP传输层,返回预设响应 type MockRoundTripper struct { MockFunc func(*http.Request) (*http.Response, error) } func (m MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return m.MockFunc(req) } func TestGetPRComments(t *testing.T) { // 临时设置测试需要的环境变量,测试结束后自动恢复 t.Setenv("GITHUB_PR_NUMBER", "123") t.Setenv("GITHUB_OWNER", "test_owner") t.Setenv("GITHUB_REPO", "test_repo") // 构造预设的接口响应数据 mockResp := `[{"id":1,"body":"test comment","user":{"login":"test_user"}}]` // 初始化mock传输层,可校验请求合法性、返回固定响应 mockTransport := MockRoundTripper{ MockFunc: func(req *http.Request) (*http.Response, error) { // 校验请求路径、方法是否符合预期 assert.Equal(t, "/repos/test_owner/test_repo/issues/123/comments", req.URL.Path) assert.Equal(t, http.MethodGet, req.Method) // 返回预设响应 return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(mockResp)), Header: http.Header{"Content-Type": []string{"application/json"}}, }, nil }, } // 构造带mock传输层的HTTP客户端,再初始化github客户端 mockHTTPClient := &http.Client{Transport: mockTransport} githubClient, err := github.NewEnterpriseClient("https://example.com/api/v3/", "https://example.com/api/v3/", mockHTTPClient) assert.NoError(t, err) // 调用待测试函数 comments, resp, err := GetPRComments(context.Background(), githubClient) // 断言结果符合预期 assert.NoError(t, err) assert.NotNil(t, resp) assert.Len(t, comments, 1) assert.Equal(t, "test comment", *comments[0].Body) assert.Equal(t, "test_user", *comments[0].User.Login) // 补充边界用例:测试非法PR号时的panic逻辑 t.Run("invalid PR number panic", func(t *testing.T) { t.Setenv("GITHUB_PR_NUMBER", "abc") assert.Panics(t, func() { GetPRComments(context.Background(), githubClient) }) }) }
方案2:抽象接口依赖注入(适合长期迭代的业务)
如果后续需要扩展更多GitHub接口能力,建议抽象依赖接口,完全和go-github实现解耦,测试更灵活:
第一步:改造业务代码,依赖抽象接口
// GitHubService 抽象业务需要的GitHub接口能力 type GitHubService interface { ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) } // 改造GetPRComments,依赖抽象接口而非具体的*github.Client func GetPRComments(ctx context.Context, ghService GitHubService) ([]*github.IssueComment, *github.Response, error) { // 原有逻辑不变,仅替换调用方法来源 opts := &github.IssueListCommentsOptions{ ListOptions: github.ListOptions{ Page: 1, PerPage: 30, }, } githubPrNumber, err := strconv.Atoi(os.Getenv("GITHUB_PR_NUMBER")) if err != nil || githubPrNumber == 0 { panic("error: GITHUB_PR_NUMBER is not numeric or empty") } comments, response, err := ghService.ListComments( ctx, os.Getenv("GITHUB_OWNER"), os.Getenv("GITHUB_REPO"), githubPrNumber, opts, ) if err != nil { return nil, nil, err } return comments, response, nil } // GitHubClientAdapter 适配真实go-github客户端,实现抽象接口 type GitHubClientAdapter struct { IssueClient *github.IssuesService } func (g *GitHubClientAdapter) ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { return g.IssueClient.ListComments(ctx, owner, repo, number, opts) } // 业务侧使用示例 // adapter := &GitHubClientAdapter{IssueClient: 真实github客户端.Issues} // comments, resp, err := GetPRComments(ctx, adapter)
第二步:编写测试用例,直接Mock接口实现
// MockGitHubService 测试用的接口实现 type MockGitHubService struct { MockListComments func(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) } func (m *MockGitHubService) ListComments(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { return m.MockListComments(ctx, owner, repo, number, opts) } func TestGetPRCommentsWithInterface(t *testing.T) { // 临时设置环境变量 t.Setenv("GITHUB_PR_NUMBER", "123") t.Setenv("GITHUB_OWNER", "test_owner") t.Setenv("GITHUB_REPO", "test_repo") // 构造mock实现,返回预设测试数据 mockService := &MockGitHubService{ MockListComments: func(ctx context.Context, owner, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { // 校验传入参数正确性 assert.Equal(t, "test_owner", owner) assert.Equal(t, "test_repo", repo) assert.Equal(t, 123, number) assert.Equal(t, 1, opts.Page) assert.Equal(t, 30, opts.PerPage) // 返回预设数据 return []*github.IssueComment{ {ID: github.Int64(1), Body: github.String("test comment"), User: &github.User{Login: github.String("test_user")}}, }, &github.Response{}, nil }, } // 调用测试函数并断言结果 comments, resp, err := GetPRComments(context.Background(), mockService) assert.NoError(t, err) assert.Len(t, comments, 1) assert.Equal(t, "test comment", *comments[0].Body) }
内容的提问来源于stack exchange,提问作者mellow-yellow
相关产品推荐
相关产品推荐

