#gRPC Test Structure Feedback Review

3 messages · Page 1 of 1 (latest)

pseudo hemlock
#

Hey all,

I am writing unit tests for my application. My application is a microservice that exposes gRPC and gRPC-Gateway as an http reverse proxy and I want my unit tests to cover my gRPC endpoints as well as the gRPC-Gateway HTTP reverse proxy since I am support HTTP as well for web clients. I'm going to walk through my testing strategy below and attach my real test code to this post since discord prevents me from posting all my code.

Alright, to start off I want to go over my application architecture. I am following the principals of uncle bobs clean architecture, so my application is split up into multiple layers where all the dependencies point inwards: Frameworks & drivers -> Usecases -> Repository. So, since I am writing unit tests I want to mock my repository layer for these tests so I can write clear assertions and have deterministic outputs.

Here I setup my gRPC service and mock my repositry, which a dependency of my usecases. Next, I create a mocked server and a client that I can pass to my test cases for testing my service functionality.

func setupServiceServer() ServiceServer {
    mockUserRepo := mock.NewMockUserRepository()
    userUseCase := usecase.NewUserUseCase(mockUserRepo)

    return NewServiceServer(userUseCase)
}

func Test_Service(t *testing.T) {
    serviceServer := setupServiceServer()

    // Setup listener
    lis := bufconn.Listen(1024 * 1024)
    t.Cleanup(func() { lis.Close() })

    // Setup gRPC server
    srv := grpc.NewServer()
    t.Cleanup(func() { srv.GracefulStop() })

    // Register Service Server
    grpc_service.RegisterServiceServer(srv, serviceServer)

    // Start Server
    go func() {
        if err := srv.Serve(lis); err != nil {
            log.Fatalf("srv.Serve %v", err)
        }
    }()

    // Dial Context
    dialer := func(context.Context, string) (net.Conn, error) {
        return lis.Dial()
    }
    conn, err := grpc.DialContext(context.Background(), "", grpc.WithContextDialer(dialer), grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("grpc.DialContext %v", err)
    }
    t.Cleanup(func() { conn.Close() })

    // Create Client
    client := grpc_service.NewServiceClient(conn)
        
        // ... More to come
}

Next, I have my tests split up into suites, where each suite contains a slice of tests that are executed. Each test has a dependency on my service client to be able to call my test gRPC server.

type testDefinition struct {
    name    string
    execute func(t *testing.T, client grpc_service.serviceClient)
}

type testSuite struct {
    name  string
    tests []testDefinition
}

var userTestSuite = testSuite{
    name: "User",
    tests: []testDefinition{
        {
            name: "must return successful response when an user is created",
            execute: func(t *testing.T, client grpc_service.ServiceClient) {
                request := &grpc_service.CreateUserRequest{
                    User: &grpc_model.User{
                        JoinDate: time.Now().Format(time.RFC3339),
                    },
                }

                resp, err := client.CreateUser(context.Background(), request)
                if err != nil {
                    t.Errorf("wanted nil error, got %v", err)
                }

                cmpopts := cmpopts.IgnoreFields(grpc_model.User{}, "Id", "state", "sizeCache", "unknownFields")
                if diff := cmp.Diff(request.User, resp.User, cmpopts); diff != "" {
                    t.Errorf("user mismatch (-want +got):\n%s", diff)
                }
            },
        },
    },
}
#

Lastly, I store all my test suites in a slice and iterate over the slice to execute all my test suites and test cases for the suite.

    for _, suite := range suites {
        for _, td := range suite.tests {
            formattedTestName := strings.Join([]string{suite.name, td.name}, "/")
            t.Run(formattedTestName, func(t *testing.T) {
                td.execute(t, affiliateClient)
            })
        }
    }

Currently, this designs feels good since it is clear on how suites group testing functionality as well as each test case has full control over the cases it is testing without trying to abstract out everything and make it more complicated. In my example above I am only testing my gRPC service with the service client, but following this same approach it would be easy enough to add in testing for the gRPC-Gateway reverse proxy since the functionality is duplicate. Let me know what you think and if you have any feedback.

heavy tide
#

what's the difference between service and usecase ?