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
你好,@drpaneas,看起来 Get 函数是在 *github.Client 的 Repositories 字段上,而不是直接在客户端上。你应该可以将你的 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)
关键点
- 定义你自己的接口:不要尝试为第三方库定义接口,而是为你自己的业务逻辑定义接口
- 依赖注入:通过接口将依赖注入到函数中
- 接口隔离:只定义你需要的方法,保持接口简洁
- 手动Mock:对于简单场景,手动创建Mock结构体
- 工具辅助:对于复杂场景,使用gomock或mockery生成Mock代码
这种模式使得测试更加简单,不需要真实的GitHub API调用,同时保持了代码的可测试性和可维护性。

