如何让单元测试中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.
相关产品推荐
相关产品推荐

