Test a gRPC Server and Client

Now that we’ve finished our gRPC server, we need some tests to check that our client and server work like we expect. We’ve already tested the details of our log’s library implementation in the library, so the tests we’re writing here are at a higher level and focus on ensuring that everything’s hooked up properly between the gRPC and library bits and that our gRPC client and server can communicate.

In the grpc directory, create a server_test.go file, and add the following code that will set up your test:

ServeRequestsWithgRPC/internal/server/server_test.go
 package​ server
 
 import​ (
 "context"
 "io/ioutil"
 "net"
 "testing"
 
 "github.com/stretchr/testify/require"
  api ​"github.com/travisjeffery/proglog/api/v1"
 "github.com/travisjeffery/proglog/internal/log"
 "google.golang.org/grpc"
 )
 
 func​ TestServer(t *testing.T) {
 for​ scenario, fn := ​range​ ​map​[​string​]​func​(
  t *testing.T,
  client api.LogClient,
  config *Config,
  ){
 "produce/consume a message to/from the log succeeeds"​:
  testProduceConsume,
 "produce/consume stream succeeds"​:
  testProduceConsumeStream,
 "consume past log boundary fails"​:
  testConsumePastBoundary,
  } {
  t.Run(scenario, ​func​(t *testing.T) {
  client, config, teardown := setupTest(t, nil)
 defer​ teardown()
  fn(t, client, config)
  })
  }
 }

TestServer(*testing.T) defines our list of test cases and then runs a subtest for each case. Add the following setupTest(*testing.T, func(*Config)) function below TestServer:

ServeRequestsWithgRPC/internal/server/server_test.go
 func​ setupTest(t *testing.T, fn ​func​(*Config)) (
  client api.LogClient,
  cfg *Config,
  teardown ​func​(),
 ) {
  t.Helper()
 
  l, err := net.Listen(​"tcp"​, ​":0"​)
  require.NoError(t, err)
 
  clientOptions := []grpc.DialOption{grpc.WithInsecure()}
  cc, err := grpc.Dial(l.Addr().String(), clientOptions...)
  require.NoError(t, err)
 
  dir, err := ioutil.TempDir(​""​, ​"server-test"​)
  require.NoError(t, err)
 
  clog, err := log.NewLog(dir, log.Config{})
  require.NoError(t, err)
 
  cfg = &Config{
  CommitLog: clog,
  }
 if​ fn != nil {
  fn(cfg)
  }
  server, err := NewGRPCServer(cfg)
  require.NoError(t, err)
 
 go​ ​func​() {
  server.Serve(l)
  }()
 
  client = api.NewLogClient(cc)
 
 return​ client, cfg, ​func​() {
  server.Stop()
  cc.Close()
  l.Close()
  clog.Remove()
  }
 }

setupTest(*testing.T, func(*Config)) is a helper function to set up each test case. Our test setup begins by creating a listener on the local network address that our server will run on. The 0 port is useful for when we don’t care what port we use since 0 will automatically assign us a free port. We then make an insecure connection to our listener and, with it, a client we’ll use to hit our server with. Next we create our server and start serving requests in a goroutine because the Serve method is a blocking call, and if we didn’t run it in a goroutine our tests further down would never run.

Now we’re ready to write some test cases. Add the following code below setupTest:

ServeRequestsWithgRPC/internal/server/server_test.go
 func​ testProduceConsume(t *testing.T, client api.LogClient, config *Config) {
  ctx := context.Background()
 
  want := &api.Record{
  Value: []​byte​(​"hello world"​),
  }
 
  produce, err := client.Produce(
  ctx,
  &api.ProduceRequest{
  Record: want,
  },
  )
  require.NoError(t, err)
 
  consume, err := client.Consume(ctx, &api.ConsumeRequest{
  Offset: produce.Offset,
  })
  require.NoError(t, err)
  require.Equal(t, want.Value, consume.Record.Value)
  require.Equal(t, want.Offset, consume.Record.Offset)
 }

testProduceConsume(*testing.T, api.LogClient, *Config) tests that producing and consuming works by using our client and server to produce a record to the log, consume it back, and then check that the record we sent is the same one we got back.

Add the following test case below testProduceConsume:

ServeRequestsWithgRPC/internal/server/server_test.go
 func​ testConsumePastBoundary(
  t *testing.T,
  client api.LogClient,
  config *Config,
 ) {
  ctx := context.Background()
 
  produce, err := client.Produce(ctx, &api.ProduceRequest{
  Record: &api.Record{
  Value: []​byte​(​"hello world"​),
  },
  })
  require.NoError(t, err)
 
  consume, err := client.Consume(ctx, &api.ConsumeRequest{
  Offset: produce.Offset + 1,
  })
 if​ consume != nil {
  t.Fatal(​"consume not nil"​)
  }
  got := grpc.Code(err)
  want := grpc.Code(api.ErrOffsetOutOfRange{}.GRPCStatus().Err())
 if​ got != want {
  t.Fatalf(​"got err: %v, want: %v"​, got, want)
  }
 }

testConsumePastBoundary(*testing.T, api.LogClient, *Config) tests that our server responds with an api.ErrOffsetOutOfRange error when a client tries to consume beyond the log’s boundaries.

We have one more test case. Put the following snippet at the bottom of the file:

ServeRequestsWithgRPC/internal/server/server_test.go
 func​ testProduceConsumeStream(
  t *testing.T,
  client api.LogClient,
  config *Config,
 ) {
  ctx := context.Background()
 
  records := []*api.Record{{
  Value: []​byte​(​"first message"​),
  Offset: 0,
  }, {
  Value: []​byte​(​"second message"​),
  Offset: 1,
  }}
 
  {
  stream, err := client.ProduceStream(ctx)
  require.NoError(t, err)
 
 for​ offset, record := ​range​ records {
  err = stream.Send(&api.ProduceRequest{
  Record: record,
  })
  require.NoError(t, err)
  res, err := stream.Recv()
  require.NoError(t, err)
 if​ res.Offset != ​uint64​(offset) {
  t.Fatalf(
 "got offset: %d, want: %d"​,
  res.Offset,
  offset,
  )
  }
  }
 
  }
 
  {
  stream, err := client.ConsumeStream(
  ctx,
  &api.ConsumeRequest{Offset: 0},
  )
  require.NoError(t, err)
 
 for​ i, record := ​range​ records {
  res, err := stream.Recv()
  require.NoError(t, err)
  require.Equal(t, res.Record, &api.Record{
  Value: record.Value,
  Offset: ​uint64​(i),
  })
  }
  }
 }

testProduceConsumeStream(*testing.T, api.LogClient, *Config) is the streaming counterpart to testProduceConsume, testing that we can produce and consume through streams.

Run $ make test to test your code. In the test output, you’ll see your TestServer test passing.

Wahoo! You’ve written and tested your first gRPC service.