Testing With Gnomock

2 minute read

Writing integration/end-to-end tests that make use of application dependencies running in Docker containers has been made popular by toolkits such as Test Containers. When working on a Go codebase, there are a few options to choose from:

Out of the these, gnomock is the one that aims to do most of the heavy-lifting on behalf of its users, by providing so-called Presets that spin up Docker containers for popular components such as Kafka, PostgreSQL, RabbitMQ, and more.

Let’s see how this would work by writing:

  • a Kinesis client capable of putting records on a stream and consuming them
  • a test where we verify that our client has indeed put a record on the stream

For this purpose, we’re going to use localstack, an excellent framework for developing applications against a local AWS stack.

Let’s start by defining our Kinesis client (the implementation details are omitted for brevity’s sake but a complete, runnable example is available here):

// file: example.go

package example

import "github.com/aws/aws-sdk-go/service/kinesis"

type KinesisClient struct {
  client *kinesis.Kinesis
}

func NewClient(endpoint *string) (*KinesisClient, error) { ... }

func (kc *KinesisClient) CreateStreamIfNotExists(name *string, shardCount *int64) error { ... }

func (kc *KinesisClient) PutRecord(stream *string, key string, data []byte) error { ... }

func (kc *KinesisClient) ConsumeRecords(streamName *string) ([]*kinesis.Record, error) { ... }

Now let’s create our end-to-end test:

// file: example_test.go

package example

import (
 "fmt"
 "github.com/orlangure/gnomock"
 "github.com/orlangure/gnomock/preset/localstack"
 "os"
 "testing"
 "time"
)

var (
 streamName         = "my-stream"
 streamShards int64 = 1
)

func TestKinesisIntegration(t *testing.T) {
  // set environment variables looked up by AWS: localstack doesn't perform any authentication, so any non-empty value will do
 _ = os.Setenv("AWS_ACCESS_KEY_ID", "x")
 _ = os.Setenv("AWS_SECRET_ACCESS_KEY", "y")

 container, err := gnomock.Start(
  localstack.Preset(localstack.WithServices(localstack.Kinesis)),
  gnomock.WithTimeout(time.Minute*2),
  gnomock.WithDebugMode(), // remove this if you don't want to see gnomock logs
 )
 if err != nil {
  t.Fatal(err.Error())
 }

  // this guarantees that we'll stop our containers even when a panic occurs
 defer func() {
  if r := recover(); r != nil {
   _ = gnomock.Stop(container)
  }
 }()

 endpoint := fmt.Sprintf("http://%s/", container.Address(localstack.APIPort))
 kc, err := Connect(&endpoint)
 if err != nil {
  t.Fatal(err.Error())
 }

 if err := kc.CreateStreamIfNotExists(&streamName, &streamShards); err != nil {
  t.Fatal(err.Error())
 }

 if err := kc.PutRecord(&streamName, "key", []byte("data")); err != nil {
  t.Fatal(err.Error())
 }

 records, err := kc.ConsumeRecords(&streamName)
 if err != nil {
  t.Fatal(err.Error())
 }

 if len(records) != 1 {
  t.Fatalf("Expected 1 record, but got %v instead\n", len(records))
 }

 // stop our container at the end of the test
 _ = gnomock.Stop(container)
}

As you can see, spinning up a fully-functional Localstack container running Kinesis only takes 8 lines of code (including error handling): what’s more, gnomock.Start(...) blocks until the container passes some predefined health checks, so that we don’t get access to it when it’s not yet ready to handle our requests.

Now we’re ready to run this test:

$ go test -v ./...
=== RUN   TestKinesisIntegration
{"L":"INFO","T":"2021-06-02T22:24:31.784+0200","M":"starting","id":"e5f2af31-4119-4441-b481-5e7bc88f9727","image":"docker.io/localstack/localstack:0.12.2","ports":{"api":{"protocol":"tcp","port":4566,"host_port":0},"web":{"protocol":"tcp","port":8080,"host_port":0}}}
{"L":"INFO","T":"2021-06-02T22:24:31.785+0200","M":"using config","id":"e5f2af31-4119-4441-b481-5e7bc88f9727","image":"docker.io/localstack/localstack:0.12.2","ports":{"api":{"protocol":"tcp","port":4566,"host_port":0},"web":{"protocol":"tcp","port":8080,"host_port":0}},"config":{"timeout":120000000000,"env":["SERVICES=kinesis"],"debug":true,"privileged":false,"container_name":"","cmd":null,"host_mounts":null,"disable_cleanup":false}}

...

--- PASS: TestKinesisIntegration (30.76s)
PASS
ok      github.com/fedragon/gnomock-kinesis-example     31.162s