Golang中如何进行Mock测试

Golang中如何进行Mock测试 我正在尝试学习如何在Go中进行模拟测试,但……我想我需要一些帮助。以下是我目前完成的内容:

我将编写一个客户端,通过Github API进行身份验证,然后获取我的“ghostfish”仓库的描述。

package main

import (
    "context"
    "fmt"
    "github.com/google/go-github/v30/github"
    "golang.org/x/oauth2"
    "log"
    "os"
)

func main() {
    // 导入你的GitHub令牌
    token := oauth2.Token{
        AccessToken: os.Getenv("GHTOKEN"),
    }
    tokenSource := oauth2.StaticTokenSource(&token)

    // 使用GitHub令牌构建oAuth2客户端
    tc := oauth2.NewClient(context.Background(), tokenSource)

    // 传递oAuth2客户端来构建GitHub客户端
    client := github.NewClient(tc)

    // 获取关于 https://github.com/drpaneas/ghostfish 仓库的信息
    repo, _, err := client.Repositories.Get(context.Background(), "drpaneas", "ghostfish")
    if err != nil {
        log.Fatal(err)
    }

    // 打印仓库的描述
    fmt.Println(repo.GetDescription())
}

到目前为止,还没有任何模拟。要运行上面的代码,你需要将一对GitHub令牌加载到环境变量中:

$ export GHTOKEN="你的令牌"

运行它:

$ go build
$ ./gh-client
用Go编写的TCP扫描器

关注点分离

将所有内容都放在一个main()函数中很难编写好的单元测试。在测试中调用main()会向GitHub的API发起多次网络请求,这不是我们的本意。让我们把代码拆分一下:

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("错误")
        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
}

好多了!定义了两个不同的函数:

  • NewGithubClient() 进行身份验证并返回一个客户端,用于后续操作,例如获取仓库列表。
  • GetUserRepo() 获取用户的仓库

注意GetUserRepo() 接受一个 *github.Client 来执行GitHub请求,这是依赖注入模式的开始。客户端被注入到将要使用它的函数中。我们能注入一个模拟客户端吗?在回答这个问题之前,先看看这个简单的测试:

// main_test.go
package main_test

import (
    . "github.com/drpaneas/gh-client"
    "os"
    "testing"
)

func TestGetUserRepos(t *testing.T) {
    os.Setenv("GHTOKEN", "假令牌")
    client := NewGithubClient()
    repo, err := GetUserRepo(client, "whatever")
    if err != nil {
        t.Errorf("期望为nil,得到 %s", err)
    }
    if repo.GetDescription() != "用Go编写的TCP扫描器" {
        t.Errorf("期望 '用Go编写的TCP扫描器',得到 %s", repo.GetDescription())
    }
}

运行它:

$ go test
--- FAIL: TestGetUserRepos (1.77s)
    main_test.go:14: 期望为nil,得到 GET https://api.github.com/repos/whatever/ghostfish: 401 Bad credentials []
    main_test.go:18: 期望 '用Go编写的TCP扫描器',得到 ''
FAIL
exit status 1
FAIL    github.com/drpaneas/gh-client        3.896s

程序返回一个错误,试图用我们伪造的 GHTOKEN 进行身份验证。我们不希望这样。

接口来救援

想象一下,我们可以为 *github.Client 创建一个具有相同 Repositories.Get() 方法和签名的模拟对象,并在测试期间将其注入到 GetUserRepo() 方法中。Go是一种静态类型语言,在当前的实现中,只有 *github.Client 可以传递给 GetUserRepo()。因此,需要重构它以接受任何类型,只要它具有 Repositories.Get() 方法,比如一个模拟客户端。

幸运的是,Go有 interface 的概念,它是一种包含一组方法签名的类型。如果任何类型实现了这些方法,它就满足该接口,并被该接口类型识别。

但是……我想不出正确的接口,因为它总是报错。以下是我的尝试(甚至无法编译):

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

// 错误 1: 不能将 'client' (类型 *github.Client) 用作类型 *GithubClient
repo, err := GetUserRepo(client, "drpaneas")

// 错误 2: 未解析的引用 'Repositories'
repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")

非常感谢任何帮助。提前感谢!


更多关于Golang中如何进行Mock测试的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

你好,@drpaneas,看起来 Get 函数是在 *github.ClientRepositories 字段上,而不是直接在客户端上。你应该可以将你的 GetUserRepo 函数修改成这样:

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
}

然后像这样使用它:

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

更多关于Golang中如何进行Mock测试的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中进行Mock测试的关键是使用接口进行依赖注入。你遇到的问题是因为github.Client的结构比较复杂,直接为其定义接口比较困难。更好的做法是为你自己的业务逻辑定义接口。

解决方案

1. 定义业务接口

首先,为你的仓库操作定义一个接口:

// repository.go
package main

import (
    "context"
    "github.com/google/go-github/v30/github"
)

// RepositoryService 定义你需要的仓库操作接口
type RepositoryService interface {
    Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error)
}

// RepositoryGetter 包装github.Client的Repositories服务
type RepositoryGetter struct {
    Repositories RepositoryService
}

// GetRepo 使用接口获取仓库
func (rg *RepositoryGetter) GetRepo(ctx context.Context, owner, repo string) (*github.Repository, error) {
    repository, _, err := rg.Repositories.Get(ctx, owner, repo)
    return repository, err
}

2. 修改业务函数使用接口

// main.go
package main

import (
    "context"
    "fmt"
    "github.com/google/go-github/v30/github"
    "golang.org/x/oauth2"
    "log"
    "os"
)

func main() {
    client := NewGithubClient()
    // 创建RepositoryGetter,注入真实的github.Client
    getter := &RepositoryGetter{
        Repositories: client.Repositories,
    }
    
    repo, err := GetUserRepo(getter, "drpaneas")
    if err != nil {
        fmt.Println("错误")
        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)
    return github.NewClient(tc)
}

// GetUserRepo 现在接受RepositoryGetter接口
func GetUserRepo(getter *RepositoryGetter, user string) (*github.Repository, error) {
    return getter.GetRepo(context.Background(), user, "ghostfish")
}

3. 创建Mock实现

// mock_repository.go
package main

import (
    "context"
    "github.com/google/go-github/v30/github"
)

// MockRepositoryService 模拟的RepositoryService实现
type MockRepositoryService struct {
    GetFunc func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error)
}

// Get 实现RepositoryService接口
func (m *MockRepositoryService) Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) {
    if m.GetFunc != nil {
        return m.GetFunc(ctx, owner, repo)
    }
    // 默认返回一个模拟的仓库
    return &github.Repository{
        Description: github.String("用Go编写的TCP扫描器"),
    }, &github.Response{}, nil
}

4. 编写测试

// main_test.go
package main

import (
    "context"
    "github.com/google/go-github/v30/github"
    "testing"
)

func TestGetUserRepo(t *testing.T) {
    // 创建Mock
    mockRepoService := &MockRepositoryService{
        GetFunc: func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) {
            // 验证参数
            if owner != "drpaneas" {
                t.Errorf("期望owner为'drpaneas', 得到 %s", owner)
            }
            if repo != "ghostfish" {
                t.Errorf("期望repo为'ghostfish', 得到 %s", repo)
            }
            
            // 返回模拟数据
            return &github.Repository{
                Description: github.String("用Go编写的TCP扫描器"),
            }, &github.Response{}, nil
        },
    }
    
    // 创建RepositoryGetter并注入Mock
    getter := &RepositoryGetter{
        Repositories: mockRepoService,
    }
    
    // 执行测试
    repo, err := GetUserRepo(getter, "drpaneas")
    if err != nil {
        t.Errorf("期望错误为nil, 得到 %v", err)
    }
    
    expectedDesc := "用Go编写的TCP扫描器"
    if repo.GetDescription() != expectedDesc {
        t.Errorf("期望描述为 '%s', 得到 '%s'", expectedDesc, repo.GetDescription())
    }
}

func TestGetUserRepo_ErrorCase(t *testing.T) {
    mockRepoService := &MockRepositoryService{
        GetFunc: func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) {
            return nil, &github.Response{}, fmt.Errorf("模拟错误")
        },
    }
    
    getter := &RepositoryGetter{
        Repositories: mockRepoService,
    }
    
    _, err := GetUserRepo(getter, "drpaneas")
    if err == nil {
        t.Error("期望返回错误, 但得到nil")
    }
}

5. 使用gomock或mockery(可选)

对于更复杂的场景,可以使用代码生成工具:

# 安装mockery
go install github.com/vektra/mockery/v2@latest

# 为接口生成mock
mockery --name=RepositoryService --output=mocks --dir=.

或者使用gomock:

// 使用gomock的示例
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockRepoService := NewMockRepositoryService(mockCtrl)
mockRepoService.EXPECT().
    Get(gomock.Any(), "drpaneas", "ghostfish").
    Return(&github.Repository{
        Description: github.String("用Go编写的TCP扫描器"),
    }, &github.Response{}, nil)

关键点

  1. 定义你自己的接口:不要尝试为第三方库定义接口,而是为你自己的业务逻辑定义接口
  2. 依赖注入:通过接口将依赖注入到函数中
  3. 接口隔离:只定义你需要的方法,保持接口简洁
  4. 手动Mock:对于简单场景,手动创建Mock结构体
  5. 工具辅助:对于复杂场景,使用gomock或mockery生成Mock代码

这种模式使得测试更加简单,不需要真实的GitHub API调用,同时保持了代码的可测试性和可维护性。

回到顶部