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)
}
},
},
},
}