因为go标准库net/http底层使用未导出的具体类型(如*http.conn或*tls.conn)实现net.conn,外部无法直接mock;正确做法是替换http.client.transport,通过自定义roundtripper拦截请求。

为什么直接 mock net.Conn 会失效
Go 标准库的 net/http 在底层调用 net.Conn 时,实际使用的是未导出的具体类型(如 http.conn 或 tls.Conn),无法被外部直接替换。试图用 interface{} 强转或反射伪造 net.Conn 实现,往往在 TLS 握手、超时控制或连接复用阶段崩溃,错误信息类似 panic: interface conversion: *http.conn is not net.Conn。
真正可控的拦截点在更高层:HTTP transport 层。你应该替换 http.Client.Transport,而不是试图 stub 底层连接对象。
- 所有 HTTP 请求都经过
RoundTrip方法,这是唯一需要实现的接口方法 - 自定义
RoundTripper可以精确返回特定状态码、延迟、空 body 或 panic(模拟网络中断) - 避免重写
net.DialContext—— 它只影响连接建立,不覆盖已建立连接的读写失败场景
用 RoundTripper 模拟磁盘写满(io.ErrNoSpace)
磁盘写满不是网络层问题,而是发生在 HTTP body 写入底层 os.File 或缓冲区时。标准 http.Transport 本身不触发磁盘 I/O,但当你用 io.Copy 把响应体写入文件,或使用 http.Response.Body 的 Read 方法配合大 buffer 时,才可能暴露该错误。
关键在于:桩函数必须作用于你实际调用的 I/O 路径上,而不是 HTTP 协议栈。
- 如果业务代码是
io.Copy(f, resp.Body),就 stubf.Write方法,让它在第 N 次调用时返回io.ErrNoSpace - 不要试图在
RoundTripper中返回一个“假装写满”的 body —— 这无法触发真实的write: no space left on device - 推荐用
io.Writer包装器封装真实文件,而非替换整个os.OpenFile—— 后者会影响测试隔离性且难以控制触发时机
Stub 函数如何避免污染全局状态
很多示例把 http.DefaultClient 的 Transport 直接赋值为测试桩,这会导致并发测试失败或干扰其他测试用例。Go 测试中应始终使用显式构造的 client 实例。
- 把 client 作为参数注入到被测函数,测试时传入带桩 transport 的 client
- 若无法修改签名,用函数变量替代全局 client:
var httpClient = http.DefaultClient,测试前用defer恢复原值 - 绝对不要在
init()或包级变量中初始化带副作用的 client —— 它会让 stub 失效且难以追踪 - 注意
http.Transport自身有连接池和 idle keep-alive,测试后需调用CloseIdleConnections()防止端口耗尽
真实故障组合场景怎么写断言
单测里只检查 status == 503 不够。磁盘写满常伴随部分写入成功、临时文件残留、重试逻辑触发等副作用,这些才是业务逻辑真正依赖的信号。
- 断言应包含:错误类型(
errors.Is(err, io.ErrNoSpace))、写入字节数(n, err := f.Write(...))、临时文件是否存在 - 对网络故障,关注是否触发了重试(检查桩 transport 的调用次数)、是否设置了正确的
context.DeadlineExceeded - 用
testify/assert的Eventually验证异步清理行为(如 defer 删除临时文件),但避免轮询 sleep —— 改用 channel 通知更可靠
最易被忽略的是:stub 的生命周期必须严格对应测试作用域。一个 transport 桩被多个测试复用,或没清空内部计数器,就会让后续测试误判失败原因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











