I've been doing Test Driven Design for a few years now despite previous predjudice. A turning point for me was, Uncle Bob's emergent algorithms and advanced TDD talk; who would have thought you can 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 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 it's own 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 becuase 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 your GOPATH and GOBIN environment variables are as the 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 server

import (
    "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 actually looking for the compilation error to be fixed 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 server

import "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 server

import (
    "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 returned it via the json packages decoding facilities. 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 to 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 marshalling 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 a great 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 respondes to a GET query there's no need to build any sort of 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!