I have a Go unit test using bufconn.Listen to test a gRPC server in-memory. With grpc.DialContext, everything works fine, but DialContext is now deprecated. When I try to replace it with grpc.NewClient, I get an error:
rpc error: code = Unavailable desc = name resolver error: produced zero addresses
I understand NewClient uses the dns resolver by default, unlike DialContext, which uses passthrough. How can I correctly use grpc.NewClient with bufconn to get the same in-memory behavior as before?
I’ve run into similar issues before, and here’s how I solved it. The core problem is that grpc.NewClient defaults to using the dns resolver, which doesn’t know how to handle bufconn because there’s no real network address. You can resolve this by using a passthrough URL, so gRPC connects directly to the in-memory listener.
Here’s a solution that worked for me:
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
var lis *bufconn.Listener
func init() {
lis = bufconn.Listen(bufSize)
}
func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
func TestGRPC(t *testing.T) {
ctx := context.Background()
// Use grpc.WithContextDialer to hook bufconn
conn, err := grpc.NewClient(ctx, "passthrough:///bufnet",
grpc.WithContextDialer(bufDialer),
grpc.WithInsecure(),
)
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
}
Key points:
-
passthrough:///bufnet tells gRPC to skip DNS resolution.
-
WithContextDialer(bufDialer) allows your client to dial the in-memory listener.
Another way to solve this is by registering a custom resolver for bufconn. This approach is especially useful if you want to simulate multiple in-memory endpoints, which could be handy for testing.
Here’s how you could go about it:
import "google.golang.org/grpc/resolver"
resolver.Register(&bufResolverBuilder{lis})
conn, err := grpc.NewClient(ctx, "bufresolver:///test", grpc.WithInsecure())
This way, you’re using a custom resolver to handle in-memory connections, which gives you flexibility in testing with multiple endpoints. The custom resolver mirrors what DialContext did implicitly.
Pros:
- Works well for multiple in-memory services.
Cons:
- It’s more boilerplate than just using
passthrough.
If you’re just looking for a drop-in replacement for DialContext, one simple way is to wrap it in a function and use grpc.NewClient. This way, your test code stays almost the same as before, but you’re now using the new API.
Here’s how you can do it:
conn, err := grpc.NewClient(ctx, "passthrough:///ignored",
grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithInsecure(),
)
This approach keeps everything the same as your old tests, but it works seamlessly with grpc.NewClient. It’s simple and doesn’t require any extra setup like custom resolvers.