While working on an internal DNSimple microservice, I found it difficult to find good documentation on how to test HTTP(S) endpoints written in Go. Most of the times the articles that I found were incomplete and were lacking some type of tests, especially integration tests.

Here's how I ended up testing my service. Hopefully by sharing this, you'll be able to save yourself some time developing tests for your next Go project. If you need inspiration for an extensive test suite, please check our Go API client that we built to interact with our domain, DNS and certificate API.

Unit Tests

Let's start with the code to test.

# microservice.go
package microservice

import (
	"fmt"
	"net/http"
)

func NewServer(port string) *http.Server {
	addr := fmt.Sprintf(":%s", port)

	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)

	return &http.Server{
		Addr:    addr,
		Handler: mux,
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

It's a simplified version of our project. We have an exported function to instantiate a HTTP server using net/http and a private function that acts as handler.

Now let's have a look at the unit test. The key part of this code is the use of the httptest package:

# microservice_test.go
package microservice

import (
	"bytes"
	"crypto/tls"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

var (
	port = "8080"
)

//
// Unit Tests
//

func TestHandler(t *testing.T) {
	expected := []byte("Hello World")

	req, err := http.NewRequest("GET", buildUrl("/"), nil)
	if err != nil {
		t.Fatal(err)
	}

	res := httptest.NewRecorder()

	handler(res, req)

	if res.Code != http.StatusOK {
		t.Errorf("Response code was %v; want 200", res.Code)
	}

	if bytes.Compare(expected, res.Body.Bytes()) != 0 {
		t.Errorf("Response body was '%v'; want '%v'", expected, res.Body)
	}
}

func buildUrl(path string) string {
	return urlFor("http", port, path)
}

func urlFor(scheme string, serverPort string, path string) string {
	return scheme + "://localhost:" + serverPort + path
}

The first few lines setup the expected body and a GET request. Then we instantiate a new *httptest.ResponseRecorder, which is part of the Go standard library for HTTP testing:

ResponseRecorder is an implementation of http.ResponseWriter that records its mutations for later inspection in tests.

With this object we can invoke our handler directly and then we can assert that the returning HTTP code and body are the expected ones.

Integration Tests

Unit tests are great, but what if we want to test the microservice with real HTTP(S) requests that include the SSL/TLS handshake?

Before starting we need a self-signed test certificate:

go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h

This command will generate two files:

➜ ls *.pem
cert.pem key.pem

Let's write our integration test now:

func TestHTTPSServer(t *testing.T) {
	srv := NewServer(httpsPort)
	go srv.ListenAndServeTLS("cert.pem", "key.pem")
	defer srv.Close()
	time.Sleep(100 * time.Millisecond)

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}
	res, err := client.Get(buildSecureUrl("/"))

	if err != nil {
		t.Fatal(err)
	}

	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		t.Errorf("Response code was %v; want 200", res.StatusCode)
	}

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		t.Fatal(err)
	}

	expected := []byte("Hello World")

	if bytes.Compare(expected, body) != 0 {
		t.Errorf("Response body was '%v'; want '%v'", expected, body)
	}
}

As first thing we build the server (srv), then we start it in a separated goroutine, otherwise the process will hang on that line. We sleep the process a little bit to allow the server to start.

Now we can construct the client with InsecureSkipVerify: true, that asks the client to not verify the signature of the given SSL certificate. Please remember that our test certificate is not signed by a known Certificate Authority (CA) but from our computer.

As last part of the test, we perform a real HTTP request (client.Get()) and we assert again the code and the response body.

Conclusion

Go is a great language to write HTTP(S) microservices, as the standard library has all the tools needed for developing and testing them.

If you want to have a look at a complete, working example, please have a look at our gist. Instead, if you need inspiration for an extensive test suite, please check our Go API client.