You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang HTTP请求单元测试问题:getVolDetails接口测试报错排查

解决getVolDetails单元测试的HTTP响应问题

我来帮你排查下这两个错误的根源,以及如何修复你的单元测试:

错误原因分析

1. 无效的URL拼接导致请求异常

你在测试里设置MAPI_ADDR的代码有问题:

os.Setenv("MAPI_ADDR", "http://"+server.URL)

httptest.Server的URL属性本身就已经包含了http://前缀(比如http://127.0.0.1:12345),所以拼接后你得到的地址是http://http://127.0.0.1:12345,这显然是无效的URL。当你的代码用这个地址发起请求时,会触发错误的路由:要么返回包含<的HTML错误页面(导致JSON解码失败,出现invalid character '<'的错误),要么请求到了不存在的域名(比如www.HugeDomains.com,这是无效域名跳转导致的超时)。

2. 原函数的错误处理逻辑存在顺序问题

你的getVolDetails函数里,先处理了resp的状态码,再检查err是否非nil,这会导致逻辑混乱:

  • 如果请求出现错误(比如超时),err非nil,但resp可能是nil,这时候你的else分支会返回err,但之后又再次判断err != nil并返回,逻辑冗余;
  • 另外,当resp.StatusCode是500或503时,你直接返回了err,但此时err可能是nil(因为请求已经收到了响应,只是状态码错误),这会导致测试中错误判断返回值。

修正后的代码

第一步:修复单元测试的URL设置

把MAPI_ADDR的设置改成直接用server.URL,不要额外加http://,同时添加环境变量清理逻辑避免影响其他测试:

os.Setenv("MAPI_ADDR", server.URL)
defer func() {
    os.Unsetenv("MAPI_ADDR")
    server.Close()
}()

第二步:修正getVolDetails的错误处理逻辑

调整错误处理的顺序,先检查请求是否出错,再处理响应状态码:

func getVolDetails(volName string, obj interface{}) error {
    addr := os.Getenv("MAPI_ADDR")
    if addr == "" {
        err := errors.New("MAPI_ADDR environment variable not set")
        fmt.Println(err)
        return err
    }
    url := addr + "/path/to/somepage/" + volName
    client := &http.Client{
        Timeout: timeout,
    }
    resp, err := client.Get(url)
    
    // 先检查请求是否出错
    if err != nil {
        fmt.Println("server not reachable:", err)
        return err
    }
    defer resp.Body.Close() // 确保响应体被关闭,放在err检查之后
    
    // 处理不同的状态码
    switch resp.StatusCode {
    case http.StatusOK:
        // 正常响应,解码JSON
        return json.NewDecoder(resp.Body).Decode(obj)
    case http.StatusInternalServerError:
        err := fmt.Errorf("VSM %s not found", volName)
        fmt.Println(err)
        return err
    case http.StatusServiceUnavailable:
        err := errors.New("server not reachable")
        fmt.Println(err)
        return err
    default:
        err := fmt.Errorf("unexpected status code: %d", resp.StatusCode)
        fmt.Println(err)
        return err
    }
}

第三步:完善单元测试的用例

添加更多测试场景,覆盖成功、500、503等情况:

func TestGetVolDetails(t *testing.T) {
    tests := map[string]struct {
        volumeName string
        mockResp   string
        mockStatus int
        wantErr    bool
        errMsg     string
    }{
        "success": {
            volumeName: "vol",
            mockResp: `{"metadata":{"annotations":{"vsm.openebs.io/targetportals":"10.98.65.136:3260","vsm.openebs.io/cluster-ips":"10.98.65.136","openebs.io/jiva-iqn":"iqn.2016-09.com.openebs.jiva:vol","deployment.kubernetes.io/revision":"1","openebs.io/storage-pool":"default","vsm.openebs.io/replica-count":"1","openebs.io/jiva-controller-status":"Running","openebs.io/volume-monitor":"false","openebs.io/replica-container-status":"Running","openebs.io/jiva-controller-cluster-ip":"10.98.65.136","openebs.io/jiva-replica-status":"Running","vsm.openebs.io/iqn":"iqn.2016-09.com.openebs.jiva:vol","openebs.io/capacity":"2G","openebs.io/jiva-controller-ips":"10.36.0.6","openebs.io/jiva-replica-ips":"10.36.0.7","vsm.openebs.io/replica-status":"Running","vsm.openebs.io/controller-status":"Running","openebs.io/controller-container-status":"Running","vsm.openebs.io/replica-ips":"10.36.0.7","openebs.io/jiva-target-portal":"10.98.65.136:3260","openebs.io/volume-type":"jiva","openebs.io/jiva-replica-count":"1","vsm.openebs.io/volume-size":"2G","vsm.openebs.io/controller-ips":"10.36.0.6"},"creationTimestamp":null,"labels":{},"name":"vol"},"status":{"Message":"","Phase":"Running","Reason":""}}`,
            mockStatus: http.StatusOK,
            wantErr:    false,
        },
        "volume_not_found_500": {
            volumeName: "missing-vol",
            mockResp:   "VSM not found",
            mockStatus: http.StatusInternalServerError,
            wantErr:    true,
            errMsg:     "VSM missing-vol not found",
        },
        "server_unavailable_503": {
            volumeName: "vol",
            mockResp:   "Server down",
            mockStatus: http.StatusServiceUnavailable,
            wantErr:    true,
            errMsg:     "server not reachable",
        },
    }

    for name, tt := range tests {
        t.Run(name, func(t *testing.T) {
            var volume v1.Volume
            // 启动测试服务器
            server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // 验证请求的URL是否正确
                expectedPath := "/path/to/somepage/" + tt.volumeName
                if r.URL.Path != expectedPath {
                    t.Fatalf("expected request path %s, got %s", expectedPath, r.URL.Path)
                }
                w.WriteHeader(tt.mockStatus)
                fmt.Fprintln(w, tt.mockResp)
            }))
            // 设置环境变量
            os.Setenv("MAPI_ADDR", server.URL)
            // 清理资源
            defer func() {
                os.Unsetenv("MAPI_ADDR")
                server.Close()
            }()

            err := getVolDetails(tt.volumeName, &volume)
            // 判断错误是否符合预期
            if (err != nil) != tt.wantErr {
                t.Fatalf("getVolDetails() error = %v, wantErr %v", err, tt.wantErr)
            }
            if tt.wantErr && err.Error() != tt.errMsg {
                t.Fatalf("getVolDetails() error message = %v, want %v", err.Error(), tt.errMsg)
            }
            // 验证成功时的解码结果
            if !tt.wantErr && volume.Name != tt.volumeName {
                t.Fatalf("getVolDetails() volume name = %v, want %v", volume.Name, tt.volumeName)
            }
        })
    }
}

总结

  • 核心问题是URL拼接错误,httptest.Server.URL自带http://前缀,不需要额外添加;
  • 调整原函数的错误处理顺序,先检查请求错误,再处理响应状态码,避免逻辑混乱;
  • 完善测试用例,覆盖不同的响应场景,确保测试的全面性。

内容的提问来源于stack exchange,提问作者Utkarsh Mani Tripathi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:01:33