golang / protobuf

Go support for Google's protocol buffers
BSD 3-Clause "New" or "Revised" License
9.64k stars 1.58k forks source link

feature: Generate test client / `bufconn` constructors #1610

Closed coxley closed 2 months ago

coxley commented 2 months ago

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.

puellanivis commented 2 months ago

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.

coxley commented 2 months ago

@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. :)

neild commented 2 months ago

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

coxley commented 2 months ago

Bah, sorry for the noise! I saw protoc-gen-go here and my brain auto-completed "grpc".

Moving!