Testing HTTP interactions in Go

Testing is an important part of software development, which gives information about the quality of a product. It is that process by which we ensure that the product we deliver meets the expectations and requirements by testing and verifying its functionality, performance and reliability.

By testing a product we also ensure that we don't introduce any regressions into our codebase.

There are different techniques which can be used when it comes to testing a software product.

For instance in unit testing we test the individual components of a product in order to determine and evaluate the functionality of a single isolated component of our product.

In integration testing for instance we test groups of components or modules as a whole in order to test the functionality and performance of our product in a real-world environment.

The topic on testing a software product has been thoroughly documented and discussed already, so for more information on this topic please refer to the software testing page on Wikipedia.

In this post we will see how we can record and replay HTTP interactions in Go in order to provide fast, deterministic and accurate testing of our product.

Recently I've been working on Gru - A simple orchestration framework written in Go, which is a personal project I work on in my spare time and I needed to get my tests done. Gru uses etcd, as the backend for coordination and management of nodes under it's control and I was in a need to record and replay all HTTP interactions between my managed nodes and clients, so that I can create tests of these interactions.

And that's how go-vcr was created. With go-vcr you can record and later on replay all HTTP interactions created by your code. The principle work of go-vcr involves creating a httptest.Server, which is used to mock server responses as if your client was talking to the real server.

By using dependency injection, we inject an http.Transport into our client, which routes all HTTP traffic through our own httptest.Server instance, which allows us to record and/or replay these HTTP interactions.

The Go code below shows how to record and replay the HTTP interactions that were performed against an etcd cluster.

package main

import (
        "log"
        "time"

        "github.com/dnaeon/go-vcr/recorder"

        "github.com/coreos/etcd/client"
        "golang.org/x/net/context"
)

func main() {
        // Start our recorder
        r, err := recorder.New("fixtures/etcd")
        if err != nil {
                log.Fatal(err)
        }
        defer r.Stop() // Make sure recorder is stopped once done with it

        // Create an etcd configuration using our transport
        cfg := client.Config{
                Endpoints:               []string{"http://127.0.0.1:2379"},
                HeaderTimeoutPerRequest: time.Second,
                Transport:               r.Transport, // Inject our transport!
        }

        // Create an etcd client using the above configuration
        c, err := client.New(cfg)
        if err != nil {
                log.Fatalf("Failed to create etcd client: %s", err)
        }

        // Get an example key from etcd
        etcdKey := "/foo"
        kapi := client.NewKeysAPI(c)
        resp, err := kapi.Get(context.Background(), etcdKey, nil)

        if err != nil {
                log.Fatalf("Failed to get etcd key %s: %s", etcdKey, err)
        }

        log.Printf("Successfully retrieved etcd key %s: %s", etcdKey, resp.Node.Value)
}

The first time we run this code the performed HTTP interactions will be recorded in the fixtures/etcd.yaml cassette file, which go-vcr will use during the next runs in order to replay the interactions during our tests.

All interactions are stored within the cassette file and are in YAML format. And here is an example cassette file generated by go-vcr for the above code.

---
version: 1
interactions:
- request:
    body: ""
    headers:
      Accept-Encoding:
      - gzip
      User-Agent:
      - Go-http-client/1.1
    url: http://127.0.0.1:2379/v2/keys/foo?quorum=false&recursive=false&sorted=false
    method: GET
  response:
    body: |
      {"action":"get","node":{"key":"/foo","value":"bar","modifiedIndex":523,"createdIndex":523}}
    headers:
      Content-Length:
      - "92"
      Content-Type:
      - application/json
      Date:
      - Wed, 16 Dec 2015 13:45:47 GMT
      X-Etcd-Cluster-Id:
      - 7e27652122e8b2ae
      X-Etcd-Index:
      - "526"
      X-Raft-Index:
      - "807547"
      X-Raft-Term:
      - "23"
    status: 200 OK
    code: 200

This cassette file now contains all HTTP interactions that our client has made and the respective responses it received from the server.

By replaying this cassette in future test runs we avoid the needed for having the real etcd cluster to be present, which essentially turns this into a unit test.

Written on December 21, 2015