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来说这相当简单,因为getProfileCustomerProfile的一个方法,我可以简单地提供一个假的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不变
  • 对核心逻辑进行单元测试
  • 模拟外部依赖
  • 不暴露测试细节给库的使用者
回到顶部