You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 23:39:02