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

如何让单元测试中Web服务响应在io.ReadAll时触发失败?

问题描述

我有一个调用Web服务的Go程序,希望编写单元测试验证错误处理逻辑。我使用net/http/httptest模拟后端服务,但尚未找到构造响应使io.ReadAll(res.Body)返回错误的方法。以下是简化的API调用代码和单元测试代码,请问如何实现该需求,或是否有更好的测试方式?

调用Web服务的简化代码

package main

import (
    "context"
    "io"
    "net/http"
)

func apiCall(url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)

    res, _ := http.DefaultClient.Do(req)

    // 希望让此步骤的io.ReadAll触发失败
    resBody, err := io.ReadAll(res.Body)

    return resBody, err
}

单元测试代码

package main

import (
    "github.com/stretchr/testify/require"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestIt(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
        // 如何让io.ReadAll(res.Body)返回错误?
    }))

    defer server.Close()

    _, err := apiCall(server.URL)
    require.Error(t, err)
}
解决方案

核心思路:Mock HTTP客户端的传输层

httptest.Server无法直接模拟响应体读取错误,更高效的方式是通过自定义http.RoundTripper来完全控制返回的响应,让响应体在读取时抛出错误。同时推荐重构代码实现依赖注入,提升可测试性。

步骤1:重构业务代码(依赖注入客户端)

修改apiCall函数,让它接受*http.Client作为参数,摆脱对http.DefaultClient的硬依赖:

package main

import (
    "context"
    "io"
    "net/http"
)

func apiCall(client *http.Client, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    res, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close() // 新增:关闭响应体避免资源泄漏

    resBody, err := io.ReadAll(res.Body)
    return resBody, err
}

步骤2:编写单元测试

自定义RoundTripper和错误读取器,模拟响应体读取失败的场景:

package main

import (
    "io"
    "net/http"
    "testing"

    "github.com/stretchr/testify/require"
)

// 自定义RoundTripper:返回带有错误读取器的响应
type errRoundTripper struct{}

func (e errRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    return &http.Response{
        StatusCode: http.StatusOK,
        Body:       io.NopCloser(errReader{}),
        Header:     make(http.Header),
    }, nil
}

// 错误读取器:读取时直接返回指定错误
type errReader struct{}

func (e errReader) Read(p []byte) (n int, err error) {
    return 0, io.ErrUnexpectedEOF // 可替换为任意需要测试的错误类型
}

func TestApiCall_ReadError(t *testing.T) {
    // 构造使用自定义传输层的客户端
    client := &http.Client{
        Transport: errRoundTripper{},
    }

    _, err := apiCall(client, "http://example.com")
    require.Error(t, err)
    require.Equal(t, io.ErrUnexpectedEOF, err)
}

备选方案(不重构原代码)

如果无法修改业务代码,可以临时替换http.DefaultClient的传输层,测试后恢复:

func TestIt(t *testing.T) {
    // 保存原传输层,测试后恢复
    originalTransport := http.DefaultClient.Transport
    defer func() {
        http.DefaultClient.Transport = originalTransport
    }()

    http.DefaultClient.Transport = errRoundTripper{}

    _, err := apiCall("http://example.com")
    require.Error(t, err)
}

注意:这种方式可能引入测试间的副作用,优先推荐依赖注入的方案。

内容的提问来源于stack exchange,提问作者Thomas W.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:05:14