feature: Generate test client / `bufconn` constructors
coxley opened this issue · comments
Summary
My teams and I find ourselves writing test scaffolding like this a lot:
package main
import (
"context"
"net"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
"github.com/stretchr/testify/require"
pb "path/to/proto/package"
)
// NewClient returns a connected gRPC client to an in-memory service
func NewClient(t testing.TB, handler pb.Server) pb.Client {
t.Helper()
srv := grpc.NewServer()
pb.RegisterServer(srv, handler)
lis := bufconn.Listen(1 << 10)
go func() {
if err := srv.Serve(lis); err != nil {
t.Logf("service exited with error: %v", err)
}
}()
t.Cleanup(func() {
srv.GracefulStop()
lis.Close()
})
conn, err := grpc.NewClient(
"bufnet",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
)
require.NoError(t, err)
t.Cleanup(func() {
conn.Close()
})
return pb.NewClient(conn)
}
We can create our own test helpers, but it's pretty unwieldy / ugly to use even with generics because of the generated factory functions: pb.RegisterServer
, pb.NewClient
. The signature needs to look something like this to get the appropriate type constraints:
func NewClient[S any, C any](
t testing.TB,
registerFn func(s grpc.ServiceRegistrar, srv S),
newClientFn func(cc grpc.ClientConnInterface) C,
handler S,
) C
I supposed we could make our protoc-gen-go-grpc-test
, but given that google.golang.org/grpc/test/bufconn
is a central package it doesn't seem completely out of place to ask for type-specific generation to make this easier.
An added benefit would be socializing in-memory testing of gRPC testing as an alternative to using mocks.
I'm not sure what the ideal signature would look like, but for conversation purposes:
// The generated file for proto package 'foo'
package foo
type FooTest struct{}
func (FooTest) Server() *grpc.Server
func (FooTest) Client() FooClient
func (FooTest) Close() error
func NewFooTest(handler FooServer, opts ...TestOption) (FooTest, error)
func WithTestDial(opts ...grpc.DialOption) TestOption
func WithTestServer(ops ...grpc.ServerOption) TestOption
func WithTestBuffer(size int) TestOption
Alternatively, this could be avoided if it was easier to use pb.RegisterServer
and pb.NewClient
in type constraints.
This sounds like a good idea, but I think you likely want to propose it to the https://github.com/grpc/grpc-go/issues board, as they’re the people who maintain the google.golang.org/grpc
packages.
@puellanivis: This would require generated code, though, no?
I'm happy to move/cross-post where necessary. I just want to make sure that throwing it over there makes sense. :)
The protoc-gen-go-grpc
gRPC code generator is part of gRPC-Go:
https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc
Bah, sorry for the noise! I saw protoc-gen-go here and my brain auto-completed "grpc".
Moving!