
用 bufconn 替换真实网络连接做 gRPC 服务端单元测试
gRPC 服务端逻辑本身不依赖网络,但默认测试要起真实监听端口、处理 TLS、处理连接生命周期,既慢又容易端口冲突。用 bufconn 可以把 client 和 server “塞进同一个内存管道”里,绕过 TCP 栈,测试快、稳定、无需端口管理。
核心思路是:用 bufconn.Listen 创建一个内存 listener,再让 gRPC server 在它上面 serve;client 则通过 bufconn.Dialer 连过去——双方走的都是 net.Conn 接口,但底层是 bytes.Buffer。
- 必须在 test 文件里 import
google.golang.org/grpc/test/bufconn(不是官方库,是 gRPC Go 的测试辅助包) -
bufconn默认 buffer 大小是 1024 字节,大消息会卡住或报rpc error: code = Internal desc = stream terminated by RST_STREAM with error code: INTERNAL_ERROR,建议初始化时显式设大点:bufconn.Listen(1024 * 1024) - server 必须调用
srv.Serve(lis)启动,且需在 goroutine 里跑,否则会阻塞测试流程 - client dial 时要用
grpc.WithContextDialer注入自定义 dialer,不能直接传地址字符串
写一个可复用的 bufconn 测试 setup 函数
每次测试都手写 listener、server 启停、dialer 构造太重复。封装成函数后,一行就能拿到已连通的 client 和 running server。
示例:
立即学习“go语言免费学习笔记(深入)”;
func newTestServerAndClient() (*grpc.ClientConn, *grpc.Server, func()) {
lis := bufconn.Listen(1024 * 1024)
srv := grpc.NewServer()
// 注册你的 service 实现,比如:pb.RegisterYourServiceServer(srv, &yourServiceImpl{})
<pre class="brush:php;toolbar:false;">done := make(chan struct{})
go func() {
if err := srv.Serve(lis); err != nil && err != grpc.ErrServerStopped {
log.Fatal(err)
}
close(done)
}()
dialer := func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
conn, err := grpc.DialContext(context.Background(), "bufconn",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(dialer),
)
if err != nil {
log.Fatal(err)
}
cleanup := func() {
srv.Stop()
<-done
conn.Close()
}
return conn, srv, cleanup}
- 注意
grpc.WithTransportCredentials(insecure.NewCredentials())是必须的,因为bufconn不走 TLS,不能用默认的credentials.NewTLS(...) - cleanup 函数要先
srv.Stop(),再等,否则 goroutine 可能还在跑,导致资源泄漏 - 别在 cleanup 里 close
lis——bufconn.Listener没有Close方法,close 它会 panic
为什么不用 grpc.Server 的 RegisterService 模拟?
有人想跳过网络层,直接用 grpc.Server 的内部注册机制 + 手动构造 *grpc.Stream 来调,这条路基本走不通。
-
grpc.Server的注册表是 unexported 字段(serviceMap),无法从外部访问或修改 -
grpc.Stream是 interface,它的 concrete type(如*transport.stream)完全未导出,也没公开构造函数 - 哪怕硬反射搞出来,后续 codec 解析、handler 调用链、context 传递都严重依赖 transport 层,极易因 gRPC 版本升级崩掉
-
bufconn是 gRPC 官方自己用于测试的方案,稳定、轻量、语义完整,比“模拟 transport”靠谱得多
常见错误:test 中 client 请求超时或返回空响应
现象是 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 后,client.YourMethod(ctx, req) 直接返回 context deadline exceeded,或者 response 字段全零值。
- 最常见原因是 server 没真正
Serve起来——忘了 go routine 包裹,或srv.Serve(lis)被 panic 中断(比如 service 注册时传了 nil 实现) - client dial 参数漏了
grpc.WithContextDialer,导致它试图解析 “bufconn” 为域名,然后卡死在 DNS 查询 - server 注册 service 时用了错误的接口类型(比如传了指针但方法集在值上),会导致 handler 根本没注册,请求进来就 404(gRPC 里表现为
UNIMPLEMENTED) - 如果 service 方法里用了
time.Sleep或阻塞 IO,而 test context timeout 太短,也会触发 deadline,这时该调小 sleep 或加大 timeout,而不是怀疑连接问题
bufconn 的边界很清晰:它只替换传输层,业务逻辑、codec、拦截器、stream 生命周期全部照常走。一旦出问题,优先检查 server 是否真在跑、client 是否真连上了、service 是否正确注册——别往内存模型或并发模型里钻太深。










