golang / protobuf

Go support for Google's protocol buffers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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!