Simple Handler TDD in GoLang

Jeremy Duvall
7Factor Software
Published in
5 min readMay 27, 2017

I’ve been doing Test Driven Design for a few years now despite previous prejudices. A turning point for me was, Uncle Bob’s emergent algorithms and advanced TDD talk; who would have thought you could back into a very efficient Sieve of Eratosthenes by writing a few tests and making them pass?

GoLang has been around for a few years and is well known to be quite the opinionated language. I’ve been working with it for a couple of months now, and I’ve grown to enjoy the flexibility, especially when it comes to TDD.

Opinions aside, let’s dive right into a simple example, shall we?

First, a quick primer on project organization. GoLang ships with its test runner invoked via, go test so we don't need to worry about installing gigantic IDEs. Something simple like VSCode, which I've become very fond of, will suffice. Additionally, any post about GoLang would not be complete without an urgent pointer to the opinionated documentation on how to write Go code. I diverge from these instructions a little because I need a workspace per client project--but the concepts are the same.

Let’s set up a workspace. My tree looks like this:

.
├── README.md
├── bin
├── gopath
├── pkg
└── src
├── glide.yaml
└── vendor

Inside the gopath file I have the following:

#!/bin/sh# Source this file in order to build the project.
export GOPATH=$(pwd)
export GOBIN=$GOPATH/bin

Source this to ensure the GOPATH and GOBIN environment variables are as they should be per the GoLang instructions. I have many projects where I cannot mix source into a single gigantic workspace, so I built this simple script so I can source gopath boot vscode, and start building.

Before I start writing anything for a new project I usually initialize any dependencies I know I’ll need by using Glide. Until the GoLang official package manager is up and running glide functions as an excellent dependency manager. For our tiny project say we wanted to use vestigo for routing--it's lightweight and easy to use. Run:

cd src && glide get github.com/husobee/vestigo && cd ..

Follow the prompts; they’re straightforward. Now, it’s time to write our first test.

mkdir -p src/jduv.me/server && touch src/jduv.me/server/healthcheck_test.go

Now open this file in your favorite editor and let’s write a simple test:

package serverimport (
"net/http"
"net/http/httptest"
"testing"
)
func Test_HealthCheckHandlerTakesRequests(context *testing.T) {
request, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
context.Fatal(err)
}
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(HealthCheckHandler)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
context.Errorf("bad response code, wanted %v got %v",
recorder.Code, http.StatusOK)
}
}

Now let’s run it.

jduv@gaspar-iii example >go test jduv.me/server
# jduv.me/server
src/jduv.me/server/healthcheck_test.go:16: undefined: HealthCheckHandler
FAIL jduv.me/server [build failed]

Yep, the test fails as we expect. Note that my test perhaps jumps ahead a tiny bit. I’m looking to fix the compilation error and I want the status code to return a 200. I think that’s ok, but some purists might be gnashing their teeth. Sorry :). Let’s fix the test:

touch src/jduv.me/server/healthcheck.go

Inside this file implement our handler in the simplest way possible to make the test pass:

package serverimport "net/http"// HealthCheckHandler returns a message signaling if the service can take
// traffic or not. This does not tell you if the service is healthy or not.
func HealthCheckHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
}

Now let’s run our test.

jduv@gaspar-iii example >go test jduv.me/server
ok jduv.me/server 0.010s

Great, it works! Now, let’s add something a little more useful. We expect the health-check handler to return a simple JSON object that looks like the following:

{ "gtg": true }

New test!

func Test_HealthCheckHandlerReturnsGTG(context *testing.T) {
request, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
context.Fatal(err)
}
expectedBody := `{"gtg":true}`
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(HealthCheckHandler)
handler.ServeHTTP(recorder, request)
if recorder.Body.String() != expectedBody {
context.Errorf("bad response body, wanted %v got %v",
recorder.Body, expectedBody)
}
}

Run it!

jduv@gaspar-iii scratch >go test jduv.me/server
--- FAIL: Test_HealthCheckHandlerReturnsGTG (0.00s)
healthcheck_test.go:36: bad response body, wanted got { "gtg": true }
FAIL
FAIL jduv.me/server 0.010s

Excellent. Let’s make it pass:

package serverimport (
"encoding/json"
"net/http"
)
// StatusMessage struct for representing a status message
type StatusMessage struct {
Gtg bool `json:"gtg"`
}
// HealthCheckHandler returns a message signaling if the service can take
// traffic or not. This does not tell you if the service is healthy or not.
func HealthCheckHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
writer.Header().Set("Content-Type", "application/json")
// Marshal the JSON message
message := StatusMessage{true}
data, err := json.Marshal(message)
// If we can't marshal this we've got a big ugly problem.
// Crash.
if err != nil {
panic(err)
}
writer.Write(data)
}

We’ve added a simple struct to represent our JSON message and decoded it via the json package. Does it pass?

jduv@gaspar-iii scratch >go test jduv.me/server
ok jduv.me/server 0.010s

It does! How much code did we cover during our adventures? Let’s find out:

jduv@gaspar-iii scratch >go test --cover jduv.me/server
ok jduv.me/server 0.011s coverage: 85.7% of statements

Not too shabby. The uncovered bits:

// If we can't marshal this we've got a big ugly problem.
// Crash.
if err != nil {
panic(err)
}

We could ignore any decoder errors altogether by using the blank _ operator, but if something explodes when I'm marshaling a static object that I just created then we probably have a developer error--so I'm cool with my code crashing...for now ;). This is an excellent example of a library system boundary that we could mock--but mocking requires we build test seams and inject dependencies. I believe this is extraneously complex for a handler that returns a simple JSON structure with one boolean flag inside it. Let's keep it simple.

One final thing before we sign off. I think there’s too much duplication in our test code. For a simple handler that responds to a GET query, there's no need to build any fancy table-based tests--we'll save that for our next installment where we do something more interesting. I want to refactor and combine both asserts into one test. Again, some purists will disagree with me as adding another assert to a test violates the blessed Single Assertion Principle. I'll leave the debate to Twitter (my handle is below); here's the finalized test:

func Test_HealthCheckHandlerRespondsAppropriately(context *testing.T) {
request, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
context.Fatal(err)
}
expectedBody := `{"gtg":true}`
recorder := httptest.NewRecorder()
handler := http.HandlerFunc(HealthCheckHandler)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
context.Errorf("bad response code, wanted %v got %v",
recorder.Code, http.StatusOK)
}
if recorder.Body.String() != expectedBody {
context.Errorf("bad response body, wanted %v got %v",
recorder.Body, expectedBody)
}
}

And does it pass?

jduv@gaspar-iii scratch >go test --cover jduv.me/server
ok jduv.me/server 0.009s coverage: 85.7% of statements

Yep. And it’s identical to the previous run. Great!

For nicer syntax highlighting view this post on my site.

--

--

Jeremy Duvall
7Factor Software

Guitarist, software engineer, coffee geek, fitness geek, and consultant.