How to test Go HTTPS Services
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 ofhttp.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.
Luca Guidi
Former astronaut, soccer player, superhero. All at the age of 10. For some reason now I write code.
We think domain management should be easy.
That's why we continue building DNSimple.
4.3 out of 5 stars.
Based on Trustpilot.com and G2.com reviews.