Golang中如何为模块/库的公共函数编写单元测试
Golang中如何为模块/库的公共函数编写单元测试 我正在开发一个Go模块,该模块是我其他Go应用程序的库。这个模块简单地获取调用Go应用程序所需的参数,调用外部服务,验证/转换数据,并返回更新后的结构体。
// 这是库的公共函数
func GetNameFromProfile(profileQueryParams ProfileQueryParams) (ProfileResponse, error) {
customerProfile := &CustomerProfile{
client: &http.Client{Timeout: 5 * time.Second},
}
// 这需要被模拟,因为我们不想在单元测试期间调用外部服务
profileResponse, err := customerProfile.getProfile(profileQueryParams)
if err != nil {
return profileResponse, err
}
// 对数据进行更多的转换/验证,然后返回响应。为简洁起见,此处省略
return profileResponse, nil
}
func (customerProfile *CustomerProfile) getProfile(profileQueryParams ProfileQueryParams) (CustomerProfileResponse, error) {
customerProfileResponse := CustomerProfileResponse{}
url := fmt.Sprintf("https://%s.externalservice.com/query", profileQueryParams.customerID)
req, err := http.NewRequest(http.MethodGet, url, nil)
// 设置请求头的辅助方法
setHeaders(req)
res, err := customerProfile.client.Do(req)
if err != nil {
return res, fmt.Errorf("调用服务器时发生网络错误。错误: %w", err)
}
defer res.Body.Close()
customerProfileResponse = someFunctionToReturnJsonResponse(res)
return customerProfileResponse, nil
}
type CustomerProfile struct {
client *http.Client
}
我的问题是,如何在不改变签名(即不添加依赖注入)的情况下对GetNameFromProfile进行单元测试?
对于getProfile来说这相当简单,因为getProfile是CustomerProfile的一个方法,我可以简单地提供一个假的client。
如果不使用依赖注入到GetNameFromProfile中,我无法理解如何模拟customerProfile.getProfile(),而它需要被模拟,以便在单元测试期间不调用外部服务。
我感觉我遗漏了什么,我是不是做错了什么?
更多关于Golang中如何为模块/库的公共函数编写单元测试的实战教程也可以访问 https://www.itying.com/category-94-b0.html
1 回复
更多关于Golang中如何为模块/库的公共函数编写单元测试的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
对于为Go模块的公共函数编写单元测试而不改变签名,可以通过接口和测试替身来实现。以下是具体方案:
1. 定义接口来抽象外部依赖
// 在库内部定义接口(不导出)
type profileFetcher interface {
getProfile(ProfileQueryParams) (CustomerProfileResponse, error)
}
// CustomerProfile实现该接口
type CustomerProfile struct {
client *http.Client
}
// 确保CustomerProfile实现接口
var _ profileFetcher = (*CustomerProfile)(nil)
2. 修改公共函数以支持测试
// 公共函数保持不变,但内部使用可替换的实现
func GetNameFromProfile(profileQueryParams ProfileQueryParams) (ProfileResponse, error) {
return getNameFromProfileWithFetcher(profileQueryParams, &CustomerProfile{
client: &http.Client{Timeout: 5 * time.Second},
})
}
// 内部可测试的函数
func getNameFromProfileWithFetcher(profileQueryParams ProfileQueryParams, fetcher profileFetcher) (ProfileResponse, error) {
profileResponse, err := fetcher.getProfile(profileQueryParams)
if err != nil {
return ProfileResponse{}, err
}
// 数据转换/验证逻辑
return convertToProfileResponse(profileResponse), nil
}
3. 创建测试替身
// 在测试文件中
type mockProfileFetcher struct {
mockGetProfile func(ProfileQueryParams) (CustomerProfileResponse, error)
}
func (m *mockProfileFetcher) getProfile(params ProfileQueryParams) (CustomerProfileResponse, error) {
if m.mockGetProfile != nil {
return m.mockGetProfile(params)
}
return CustomerProfileResponse{}, nil
}
4. 编写单元测试
func TestGetNameFromProfile(t *testing.T) {
tests := []struct {
name string
setupMock func(*mockProfileFetcher)
input ProfileQueryParams
expectedOutput ProfileResponse
expectError bool
}{
{
name: "成功获取profile",
setupMock: func(m *mockProfileFetcher) {
m.mockGetProfile = func(params ProfileQueryParams) (CustomerProfileResponse, error) {
return CustomerProfileResponse{
Name: "John Doe",
Age: 30,
}, nil
}
},
input: ProfileQueryParams{CustomerID: "123"},
expectedOutput: ProfileResponse{
FullName: "John Doe",
Age: 30,
},
expectError: false,
},
{
name: "外部服务返回错误",
setupMock: func(m *mockProfileFetcher) {
m.mockGetProfile = func(params ProfileQueryParams) (CustomerProfileResponse, error) {
return CustomerProfileResponse{}, errors.New("服务不可用")
}
},
input: ProfileQueryParams{CustomerID: "456"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockFetcher := &mockProfileFetcher{}
tt.setupMock(mockFetcher)
// 测试内部函数
result, err := getNameFromProfileWithFetcher(tt.input, mockFetcher)
if tt.expectError {
if err == nil {
t.Error("预期错误但未收到")
}
return
}
if err != nil {
t.Errorf("意外错误: %v", err)
}
if result != tt.expectedOutput {
t.Errorf("预期 %v, 得到 %v", tt.expectedOutput, result)
}
})
}
}
5. 使用构建标签隔离测试
// 在库的根目录创建 internal_test 包
// internal/getname_test.go
package internal
import (
"testing"
"yourmodule/internal"
)
// 这里可以测试内部函数
func TestGetNameFromProfileWithFetcher(t *testing.T) {
// 测试逻辑
}
6. 替代方案:使用httptest
如果希望保持简单,可以直接测试CustomerProfile:
func TestCustomerProfile_GetProfile(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(CustomerProfileResponse{
Name: "Test User",
Age: 25,
})
}))
defer server.Close()
profile := &CustomerProfile{
client: server.Client(),
}
// 修改CustomerProfile以接受基础URL
result, err := profile.getProfileWithURL(ProfileQueryParams{
CustomerID: "test",
}, server.URL)
if err != nil {
t.Errorf("意外错误: %v", err)
}
if result.Name != "Test User" {
t.Errorf("预期 Test User, 得到 %s", result.Name)
}
}
这种方法允许你:
- 保持公共API不变
- 对核心逻辑进行单元测试
- 模拟外部依赖
- 不暴露测试细节给库的使用者

