Golang中如何实现Mock测试以及处理0%覆盖率问题?

Golang中如何实现Mock测试以及处理0%覆盖率问题? 我有一段与GitHub API通信的小代码。它使用我的Token进行身份验证。代码从我的shell环境中读取token。

export GHTOKEN=“foobar”

然后,在完成身份验证后,它会访问我的一个GitHub仓库(具体是名为"ghostfish"的那个),并获取其描述信息。

因此,我想为它编写一些测试。就在这时,所有的困惑开始了……我不想用我的测试去打扰GitHub,所以我希望以某种方式模拟函数,以避免访问实际的第三方服务——也就是GitHub API。

我做了以下操作:

  • 我将我的函数转换成了方法
  • 我创建了一个实现这些方法的接口
  • 我使用 mockgen 来生成模拟。具体的命令是:mockgen -source=main.go -destination=mocks/mock_githubClient.go -package=$GOPACKAGE

然后,当我测试覆盖率时,我得到了0%。

$ go tool cover -func cover.out
github.com/testf/main.go:24:    main            0.0%
github.com/testf/main.go:34:    Authenticated   0.0%
github.com/testf/main.go:44:    GetUserRepo     0.0%
total:                          (statements)    0.0%

这怎么可能?我很困惑……

如果你想看一下,这是代码:

main.go

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/google/go-github/v30/github"
	"golang.org/x/oauth2"
)

var (
	GithubClient githubClientInterface = &githubClient{}
)

type githubClientInterface interface {
	Authenticated(env string) *github.Client
	GetUserRepo(client *github.Client, user string) (*github.Repository, error)
}

type githubClient struct{}

func main() {
	client := GithubClient.Authenticated("GHTOKEN")
	repo, err := GithubClient.GetUserRepo(client, "drpaneas")
	if err != nil {
		fmt.Println("Error")
		log.Fatal(err)
	}
	fmt.Println(repo.GetDescription())
}

func (c *githubClient) Authenticated(env string) *github.Client {
	token := oauth2.Token{
		AccessToken: os.Getenv(env),
	}
	tokenSource := oauth2.StaticTokenSource(&token)
	tc := oauth2.NewClient(context.Background(), tokenSource)
	client := github.NewClient(tc)
	return client
}

func (c *githubClient) GetUserRepo(client *github.Client, user string) (*github.Repository, error) {
	repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")
	if err != nil {
		return nil, err
	}
	return repo, err
}

main_test.go

package main

import (
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/go-github/v30/github"
	mocks "github.com/testf/mocks"
)

func TestWithGoMock(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockgithubClient := mocks.NewMockgithubClientInterface(mockCtrl)
	c := &github.Client{}
	mockgithubClient.EXPECT().Authenticated("whatever").AnyTimes().Return(c)
	client := mockgithubClient.Authenticated("whatever")

	s := "something"
	var x *string
	x = &s
	r := &github.Repository{Description: x}
	mockgithubClient.EXPECT().GetUserRepo(client, "drpaneas").AnyTimes().Return(r, nil)
	repo, _ := mockgithubClient.GetUserRepo(client, "drpaneas")
	output := fmt.Sprintf("%s", (repo.GetDescription()))
	if output != "something" {
		t.Error("It cannot fetch the repository name")
	}
}

mocks/mock_githubClient.go

// Code generated by MockGen. DO NOT EDIT.
// Source: main.go

// Package mock_main is a generated GoMock package.
package mock_main

import (
	gomock "github.com/golang/mock/gomock"
	github "github.com/google/go-github/v30/github"
	reflect "reflect"
)

// MockgithubClientInterface is a mock of githubClientInterface interface
type MockgithubClientInterface struct {
	ctrl     *gomock.Controller
	recorder *MockgithubClientInterfaceMockRecorder
}

// MockgithubClientInterfaceMockRecorder is the mock recorder for MockgithubClientInterface
type MockgithubClientInterfaceMockRecorder struct {
	mock *MockgithubClientInterface
}

// NewMockgithubClientInterface creates a new mock instance
func NewMockgithubClientInterface(ctrl *gomock.Controller) *MockgithubClientInterface {
	mock := &MockgithubClientInterface{ctrl: ctrl}
	mock.recorder = &MockgithubClientInterfaceMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockgithubClientInterface) EXPECT() *MockgithubClientInterfaceMockRecorder {
	return m.recorder
}

// Authenticated mocks base method
func (m *MockgithubClientInterface) Authenticated(env string) *github.Client {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Authenticated", env)
	ret0, _ := ret[0].(*github.Client)
	return ret0
}

// Authenticated indicates an expected call of Authenticated
func (mr *MockgithubClientInterfaceMockRecorder) Authenticated(env interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticated", reflect.TypeOf((*MockgithubClientInterface)(nil).Authenticated), env)
}

// GetUserRepo mocks base method
func (m *MockgithubClientInterface) GetUserRepo(client *github.Client, user string) (*github.Repository, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetUserRepo", client, user)
	ret0, _ := ret[0].(*github.Repository)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetUserRepo indicates an expected call of GetUserRepo
func (mr *MockgithubClientInterfaceMockRecorder) GetUserRepo(client, user interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserRepo", reflect.TypeOf((*MockgithubClientInterface)(nil).GetUserRepo), client, user)
}

更多关于Golang中如何实现Mock测试以及处理0%覆盖率问题?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中如何实现Mock测试以及处理0%覆盖率问题?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你的测试覆盖率为0%是因为测试代码没有调用实际的 githubClient 方法,而是调用了模拟对象的方法。覆盖率工具只统计实际代码的执行情况,而你的测试完全运行在模拟环境中。

问题分析

  1. 测试中使用的都是模拟对象mockgithubClient 是模拟对象,不会执行 githubClient 的实际方法
  2. 全局变量 GithubClient 未被替换:在 main() 函数中仍然使用原始的 GithubClient 实现
  3. 测试与生产代码分离:测试没有触发任何生产代码的执行

解决方案

方案1:使用依赖注入替换全局变量

修改 main.go,使其支持依赖注入:

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/google/go-github/v30/github"
	"golang.org/x/oauth2"
)

type githubClientInterface interface {
	Authenticated(env string) *github.Client
	GetUserRepo(client *github.Client, user string) (*github.Repository, error)
}

type githubClient struct{}

func (c *githubClient) Authenticated(env string) *github.Client {
	token := oauth2.Token{
		AccessToken: os.Getenv(env),
	}
	tokenSource := oauth2.StaticTokenSource(&token)
	tc := oauth2.NewClient(context.Background(), tokenSource)
	client := github.NewClient(tc)
	return client
}

func (c *githubClient) GetUserRepo(client *github.Client, user string) (*github.Repository, error) {
	repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")
	if err != nil {
		return nil, err
	}
	return repo, err
}

// 主逻辑函数,接受接口作为参数
func run(githubClient githubClientInterface) error {
	client := githubClient.Authenticated("GHTOKEN")
	repo, err := githubClient.GetUserRepo(client, "drpaneas")
	if err != nil {
		return err
	}
	fmt.Println(repo.GetDescription())
	return nil
}

func main() {
	client := &githubClient{}
	if err := run(client); err != nil {
		log.Fatal(err)
	}
}

修改 main_test.go

package main

import (
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/go-github/v30/github"
)

func TestRun(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockClient := NewMockgithubClientInterface(mockCtrl)
	
	// 设置模拟期望
	expectedClient := &github.Client{}
	mockClient.EXPECT().Authenticated("GHTOKEN").Return(expectedClient)
	
	expectedRepo := &github.Repository{}
	expectedDescription := "Test Repository"
	expectedRepo.Description = &expectedDescription
	
	mockClient.EXPECT().GetUserRepo(expectedClient, "drpaneas").Return(expectedRepo, nil)
	
	// 调用实际逻辑函数,传入模拟对象
	err := run(mockClient)
	if err != nil {
		t.Errorf("run() error = %v", err)
	}
}

// 测试实际实现
func TestGithubClient_Authenticated(t *testing.T) {
	// 设置环境变量
	t.Setenv("GHTOKEN", "test-token")
	
	client := &githubClient{}
	result := client.Authenticated("GHTOKEN")
	
	if result == nil {
		t.Error("Authenticated() returned nil client")
	}
}

func TestGithubClient_GetUserRepo(t *testing.T) {
	// 这个测试需要真正的GitHub客户端,可以跳过或使用集成测试
	t.Skip("需要真正的GitHub客户端,跳过单元测试")
}

方案2:使用可替换的全局变量(更接近你原来的设计)

修改 main.go

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/google/go-github/v30/github"
	"golang.org/x/oauth2"
)

// 可替换的全局变量
var GithubClient githubClientInterface = &githubClient{}

type githubClientInterface interface {
	Authenticated(env string) *github.Client
	GetUserRepo(client *github.Client, user string) (*github.Repository, error)
}

type githubClient struct{}

func main() {
	client := GithubClient.Authenticated("GHTOKEN")
	repo, err := GithubClient.GetUserRepo(client, "drpaneas")
	if err != nil {
		fmt.Println("Error")
		log.Fatal(err)
	}
	fmt.Println(repo.GetDescription())
}

func (c *githubClient) Authenticated(env string) *github.Client {
	token := oauth2.Token{
		AccessToken: os.Getenv(env),
	}
	tokenSource := oauth2.StaticTokenSource(&token)
	tc := oauth2.NewClient(context.Background(), tokenSource)
	client := github.NewClient(tc)
	return client
}

func (c *githubClient) GetUserRepo(client *github.Client, user string) (*github.Repository, error) {
	repo, _, err := client.Repositories.Get(context.Background(), user, "ghostfish")
	if err != nil {
		return nil, err
	}
	return repo, err
}

修改 main_test.go

package main

import (
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/go-github/v30/github"
)

func TestMainWithMock(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	
	// 保存原始实现
	originalClient := GithubClient
	
	// 创建模拟对象
	mockClient := NewMockgithubClientInterface(mockCtrl)
	
	// 替换全局变量
	GithubClient = mockClient
	
	// 测试完成后恢复
	defer func() {
		GithubClient = originalClient
	}()
	
	// 设置模拟期望
	expectedClient := &github.Client{}
	mockClient.EXPECT().Authenticated("GHTOKEN").Return(expectedClient)
	
	expectedRepo := &github.Repository{}
	expectedDescription := "Test Repository Description"
	expectedRepo.Description = &expectedDescription
	
	mockClient.EXPECT().GetUserRepo(expectedClient, "drpaneas").Return(expectedRepo, nil)
	
	// 由于main()会退出程序,我们需要测试实际的方法
	client := GithubClient.Authenticated("GHTOKEN")
	repo, err := GithubClient.GetUserRepo(client, "drpaneas")
	
	if err != nil {
		t.Errorf("GetUserRepo() error = %v", err)
	}
	
	if repo.GetDescription() != expectedDescription {
		t.Errorf("GetDescription() = %v, want %v", repo.GetDescription(), expectedDescription)
	}
}

// 测试实际实现
func TestGithubClient_Authenticated(t *testing.T) {
	t.Setenv("GHTOKEN", "test-token-123")
	
	client := &githubClient{}
	result := client.Authenticated("GHTOKEN")
	
	if result == nil {
		t.Error("Authenticated() returned nil")
	}
}

生成模拟文件的命令

确保使用正确的包名:

mockgen -source=main.go -destination=mocks/mock_githubClient.go -package=main

运行测试和覆盖率

# 运行测试
go test -v ./...

# 生成覆盖率报告
go test -coverprofile=cover.out ./...

# 查看覆盖率
go tool cover -func cover.out

# 查看HTML覆盖率报告
go tool cover -html=cover.out

这样修改后,你的测试将:

  1. 实际调用 githubClient 的方法(在测试实际实现时)
  2. 使用模拟对象测试业务逻辑
  3. 获得正确的覆盖率统计
回到顶部