Interfaces and Composition for Effective Unit Testing in Golang
Go programs, when properly implemented, are fairly simple to test programatically. Go unit tests offer:
- Increased enforcement of behavior expectations beyond the compiler (providing critical assurance around rapidly changing code paths)
- Fast execution speed (many popular modules' tests can execute completely in seconds)
- Easy integration into CI pipelines (
go test
is built right in) - Programmatic race condition detection through the
-race
flag
Consequently, they are by far one of the best ways to ensure code quality and prevent regressions. Tragically, however, unit testing seems to often be one of the most neglected aspects of any given Go project. It is often slept on until the effort required to implement it properly is Herculean.
This tendency seems at least partly due to a lack of quality resources explaining how to properly structure a Go program to be tested, and examples of doing so. This is a guide attempting to provide both and to increase the general quality of programs available in the Go community.
Don’t let the compiler lull you into a false sense of security: you’ll be glad you began testing sooner rather than later.
Overview
In this article I will walk you through:
- Concepts for ensuring testability
- Four concrete examples to learn how to test effectively in Go
By the end, you should be armed with the knowledge you need in order to go forth and test.
Concepts
If you do not structure and test your program properly from the start, it will be astronomically more difficult to test down the road. This is a fairly universal axiom of programming but seems particularly true of Go testing due to its opinionated nature.
The three most important concepts to be able to use fluidly in order to test Go programs effectively are:
- Using interfaces in your Go code
- Constructing higher-level interfaces through composition (embedding)
- Becoming familiar with
go test
and thetesting
module
Let’s take a look at each one and why it is important.
Using interfaces
You may be familiar with the use of interfaces from working through the Go walkthrough or from the official documentation. What you may not be familiar with is why interfaces are so important and why you should begin using your own interfaces as soon as possible.
For those who are unfamiliar with interfaces, I highly recommend you read the official documentation to understand how they work (I also have an article on interfaces).
Long story short:
- Interfaces let you define a set of methods a type (often
struct
) must define to be considered an implementation of that interface. - When any given type implements all the methods of that interface, the Go compiler automatically knows that it is allowed to be used as that type.
This is used to great effect in the Go standard library so that, for instance,
the same interface in database/sql
can be used to write functionality that
can interact with a variety of different databases using the same code.
Fledgling Go programmers may be familiar with writing unit tests in other languages such as Java, Python, or PHP, and using “stubs” or “mocks” to fake out the results of a method call and explore the various code paths you want to exercise in a fine-grained way. What many don’t seem to realize, however, is that interfaces can and should be used for this same type of operation in Go.
Due to being built into the language and supported in a huge way by the standard library, interfaces offer the test author in Go a vast amount of power and flexibility when used the correct way. One can wrap operations which are outside of the scope of a given test in an interface, and selectively re-implement them for the relevant tests. This allows the author to control every aspect of the program’s behavior within the test suite.
Using composition
Interfaces are great for increased flexibility and control, but they are not
enough on their own. Consider, for instance, the case where we have a struct
which exposes a large set of methods to external consumers but also relies on
these methods internally for some operations. We cannot wrap the whole object
in an interface to control the methods it relies on, since this would require
implementing the method we are trying to test.
Consequently, it becomes critical to compose larger interfaces from smaller ones through embedding to enable control of the methods which we do want to change while still being able to test the ones we don’t want to change. This is a bit easier to see in an actual example, so I will eschew further abstract discussion until later in the article.
go test
and testing
This may seem obvious, but at least skimming the documentation for the go test
command and the testing
package and familiarizing yourself with how
each works will make you much more effective at unit testing. The tools and
library can seem a bit quirky if you are not familiar with them or with Go
tooling in general (though they are very nice once acclimated).
It might be tempting to reach for a 3rd party package which promises to help with testing, but I highly encourage you to avoid doing so until you have a handle on the basics and are absolutely certain that the dependency will bring you more benefit than headaches.
Please, please, read the docs, but the basic knowledge you need to get started is:
- For any given file
foo.go
, the test is placed in a file in the same directory calledfoo_test.go
. go test .
runs the unit tests in the current directory.go test ./...
will run the tests in the current directory and every directory underneath it.go test foo_test.go
not work because the file under test is not included.- The
-v
flag forgo test
is useful because it prints out verbose output (the result of each individual test). - Tests are functions which accept a
testing.T
struct pointer as an argument and are idiomatically calledTestFoo
, whereFoo
is the name of the function being tested. - You usually do not assert that conditions you expect are true like you may be
accustomed to; rather, you fail the test by calling
t.Fatal
if you encounter conditions which are different than you expect. - Displaying output in tests may not work how you
expect
– use the
t.Log
and/ort.Logf
methods if you need to print information in a test.
Examples
Enough discussion of fundamentals; let’s write some tests.
If you are curious to check out the source code in its final form for the following examples, they are organized in separate directories at https://github.com/nathanleclaire/testing-article.
Example #1: Hello, Testing!
I’m assuming that you have Go installed and configured for these exercises.
Make a new Go package in your GOPATH
, e.g. I would execute:
Create a file hello.go
:
package main
import (
"fmt"
)
func hello() string {
return "Hello, Testing!"
}
func main() {
fmt.Println(hello())
}
Now let’s write a test for hello.go
.
Make a file hello_test.go
in the same directory:
package main
import (
"testing"
)
func TestHello(t *testing.T) {
expectedStr := "Hello, Testing!"
result := hello()
if result != expectedStr {
t.Fatalf("Expected %s, got %s", expectedStr, result)
}
}
Hopefully this test should be pretty self-explanatory. We inject an instance
of *testing.T
to the test, and this is used to control the test flow and
output. We set what our expectations for the function call are in one
variable, and then check it against what the function actually returns.
To run the test:
Great, now let’s try something more complex.
Example #2: Using an interface to mock results
Let’s say that as part of a program we wanted to get some data from the GitHub API. In this case, let’s say we wanted to query the git tag which the latest release of a given repo corresponds to.
We could write some code like the following to do so:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
type ReleasesInfo struct {
Id uint `json:"id"`
TagName string `json:"tag_name"`
}
// Function to actually query the GitHub API for the release information.
func getLatestReleaseTag(repo string) (string, error) {
apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)
response, err := http.Get(apiUrl)
if err != nil {
return "", err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
releases := []ReleasesInfo{}
if err := json.Unmarshal(body, &releases); err != nil {
return "", err
}
tag := releases[0].TagName
return tag, nil
}
// Function to get the message to display to the end user.
func getReleaseTagMessage(repo string) (string, error) {
tag, err := getLatestReleaseTag(repo)
if err != nil {
return "", fmt.Errorf("Error querying GitHub API: %s", err)
}
return fmt.Sprintf("The latest release is %s", tag), nil
}
func main() {
msg, err := getReleaseTagMessage("docker/machine")
if err != nil {
fmt.Fprintln(os.Stderr, msg)
}
fmt.Println(msg)
}
And, in fact, this is commonly how one will see Go programs structured in the wild.
But it is not very testable. If we were to test the getLatestReleaseTag
function directly, our test would fail if the GitHub API went down or if GitHub
decided to rate limit us (which is likely if running the test frequently such
as in CI environments). Additionally, we’d have to update the test every time
that the latest release tag changed. Yuck.
What to do? We can re-define the way that this is implemented to be a lot more
testable. If we query the GitHub API through an interface
instead of
directly through a function call, then we can actually control the result of
what will be returned through our test.
Let’s redefine the program a bit to have an interface, ReleaseInfoer
, of
which one implementation can be GithubReleaseInfoer
. ReleaseInfoer
only
has one method, GetLatestReleaseTag
, which is similar in nature to our
function above (it accepts a repository name as an argument and returns a
string
and/or error
for the result).
The interface
definition looks like:
type ReleaseInfoer interface {
GetLatestReleaseTag(string) (string, error)
}
Then, we can update our above bare function call to be a method on a
GithubReleaseInfoer
struct instead:
type GithubReleaseInfoer struct {}
// Function to actually query the GitHub API for the release information.
func (gh GithubReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {
// ... same code as above
}
And, consequently, update the getReleaseTagMessage
and main
functions like
so:
// Function to get the message to display to the end user.
func getReleaseTagMessage(ri ReleaseInfoer, repo string) (string, error) {
tag, err := ri.GetLatestReleaseTag(repo)
if err != nil {
return "", fmt.Errorf("Error query GitHub API: %s", err)
}
return fmt.Sprintf("The latest release is %s", tag), nil
}
func main() {
gh := GithubReleaseInfoer{}
msg, err := getReleaseTagMessage(gh, "docker/machine")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(msg)
}
Why bother? Well, now we can actually test the getReleaseTagMessage
function
by defining a new struct that fulfills the ReleaseInfoer
interface with a
method which we control completely. That way, at test time, we can ensure that
the behavior of the method which we depend on is exactly as we expect.
We could define a FakeReleaseInfoer
struct which behaves however we want. We
simply define what to return in the parameters of the struct.
package main
import "testing"
type FakeReleaseInfoer struct {
Tag string
Err error
}
func (f FakeReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {
if f.Err != nil {
return "", f.Err
}
return f.Tag, nil
}
func TestGetReleaseTagMessage(t *testing.T) {
f := FakeReleaseInfoer{
Tag: "v0.1.0",
Err: nil,
}
expectedMsg := "The latest release is v0.1.0"
msg, err := getReleaseTagMessage(f, "dev/null")
if err != nil {
t.Fatalf("Expected err to be nil but it was %s", err)
}
if expectedMsg != msg {
t.Fatalf("Expected %s but got %s", expectedMsg, msg)
}
}
You can see above that the FakeReleaseInfoer
struct was set to return
"v0.1.0"
and the returned error was set to nil.
That’s great for this case, but notice we’re not testing our error return. It would be preferable to cover this case as well.
Is there any way to express the various paths and return possibilities for this function concisely in our unit tests? Certainly. We can test a wide variety of cases in one function with an anonymous struct slice (thanks to Andrew Gerrand’s talk and David Cheney’s article on test tables for this idea).
func TestGetReleaseTagMessage(t *testing.T) {
cases := []struct {
f FakeReleaseInfoer
repo string
expectedMsg string
expectedErr error
}{
{
f: FakeReleaseInfoer{
Tag: "v0.1.0",
Err: nil,
},
repo: "doesnt/matter",
expectedMsg: "The latest release is v0.1.0",
expectedErr: nil,
},
{
f: FakeReleaseInfoer{
Tag: "v0.1.0",
Err: errors.New("TCP timeout"),
},
repo: "doesnt/foo",
expectedMsg: "",
expectedErr: errors.New("Error querying GitHub API: TCP timeout"),
},
}
for _, c := range cases {
msg, err := getReleaseTagMessage(c.f, c.repo)
if !reflect.DeepEqual(err, c.expectedErr) {
t.Errorf("Expected err to be %q but it was %q", c.expectedErr, err)
}
if c.expectedMsg != msg {
t.Errorf("Expected %q but got %q", c.expectedMsg, msg)
}
}
}
Note the use of reflect.DeepEqual
there. It’s a useful
method from the standard library
which will check if two structs are equal by value. It’s used to check error
equality here, but it also could be used to compare the contents of two
struct
s. Just ==
alone won’t work for equality for this due to the use of
errors.New
(I tried using the Error
method but that doesn’t work with nil
value errors, so if anyone has better ideas please mention it in the comments).
Something to take note of is that this technique can be used to gain more
control over 3rd party libraries in tests. For instance, Sam Alba’s Golang
Docker client will give you a type DockerClient struct
to interact with, which is not easily mock-able for tests.
But you could create a type DockerClient interface
in your own module which
specifies the methods you are using on dockerclient.DockerClient
as the
things to implement, use that in your code instead, and then create your own
version of that interface for testing.
Aside from the benefits to testability which I’m focusing on here, using
interfaces can potentially be a huge boon for future extensibility of your
program. If you have structured every component which interacts with the
GitHub API as working through an interface, for instance, you won’t need to
change your program’s architecture at all to add support for another source
code hosting platform. You could simply implement a BitbucketReleaseInfoer
and use that wherever you want to wrap the Bitbucket API instead of GitHub.
Granted, this type of wrapper abstraction won’t work for every use case, but it
can be used powerfully to mock out external and internal dependencies.
Example #3: Using composition to test a large struct
The above example illustrates an introductory concept which can be very useful,
but sometimes we might want to mock out parts of one struct
which depend on
each other and test each piece separately.
If you find yourself with an interface
or struct
which is starting to get
larger in terms of the number of methods exposed, it might be a good candidate
for breaking into several smaller interfaces
and
embedding them. For
instance, let’s suppose that we have a Job
interface which exposes a Log
method both internally and externally to the structure. Any interface
can be
passed to this method with a variable number of arguments. It also provides
supported for Run
ing, Suspend
ing, and Resume
ing jobs.
type Job interface {
Log(...interface{})
Suspend() error
Resume() error
Run() error
}
If we are working on developing a struct
which implements this interface
,
we may want to use the Log
method inside of the Suspend
and Resume
methods to keep track of what has happened. Therefore, faking out the whole
interface like in the previous example won’t work. How do we test the whole
structure while mocking out only part of the interface?
We can do so by defining several smaller interfaces and using composition.
Consider an implementation of a Job
, PollerJob
, which can be used for
questionable homebrew system monitoring software. My first crack at coding it
was this:
package main
import (
"log"
"net/http"
"time"
)
type Job interface {
Log(...interface{})
Suspend() error
Resume() error
Run() error
}
type PollerJob struct {
suspend chan bool
resume chan bool
resourceUrl string
inMemLog string
}
func NewPollerJob(resourceUrl string) PollerJob {
return PollerJob{
resourceUrl: resourceUrl,
suspend: make(chan bool),
resume: make(chan bool),
}
}
func (p PollerJob) Log(args ...interface{}) {
log.Println(args...)
}
func (p PollerJob) Suspend() error {
p.suspend <- true
return nil
}
func (p PollerJob) PollServer() error {
resp, err := http.Get(p.resourceUrl)
if err != nil {
return err
}
p.Log(p.resourceUrl, "--", resp.Status)
return nil
}
func (p PollerJob) Run() error {
for {
select {
case <-p.suspend:
<-p.resume
default:
if err := p.PollServer(); err != nil {
p.Log("Error trying to get resource: ", err)
}
time.Sleep(1 * time.Second)
}
}
}
func (p PollerJob) Resume() error {
p.resume <- true
return nil
}
func main() {
p := NewPollerJob("https://nathanleclaire.com")
go p.Run()
time.Sleep(5 * time.Second)
p.Log("Suspending monitoring of server for 5 seconds...")
p.Suspend()
time.Sleep(5 * time.Second)
p.Log("Resuming job...")
p.Resume()
// Wait for a bit before exiting
time.Sleep(5 * time.Second)
}
The output of the above program, when run, looks like:
If we want to test the various complex interactions at play here, how do we do so? With everything in one structure, it seems to be daunting to test each component of the program in isolation and without using external resources.
The solution is to break the higher-level Job
interface into several other
interfaces and embed them all into the PollerJob
struct, allowing us to mock
out each piece in isolation when we do testing.
We can break the Job
interface into a few different interfaces like so:
type Logger interface {
Log(...interface{})
}
type SuspendResumer interface {
Suspend() error
Resume() error
}
type Job interface {
Logger
SuspendResumer
Run() error
}
You can see that there is an interface SuspendResumer
for handling
suspend/resume functionality, and an interface Log
whose only purpose is to
manage the Log
method. Additionally, we will create a PollServer
interface
for controlling the status calls to the server we are polling:
type ServerPoller interface {
PollServer() (string, error)
}
With all of these component interfaces in place, we can begin re-constructing
our PollerJob
implementation of the Job
interface. By embedding Logger
and ServerPoller
(both interfaces) and a pointer to a PollSuspendResumer
struct, we ensure that the compiler is satisfied for the definition of
PollerJob
as a Job
. We provide a NewPollerJob
function which will
provide an instance of the struct with all of the components set up and
initialized properly. Notice that we use our own implementation of the
components such as Logger
in the struct literal that this function returns.
type PollerLogger struct{}
type URLServerPoller struct {
resourceUrl string
}
type PollSuspendResumer struct {
SuspendCh chan bool
ResumeCh chan bool
}
type PollerJob struct {
WaitDuration time.Duration
ServerPoller
Logger
*PollSuspendResumer
}
func NewPollerJob(resourceUrl string, waitDuration time.Duration) PollerJob {
return PollerJob{
WaitDuration: waitDuration,
Logger: &PollerLogger{},
ServerPoller: &URLServerPoller{
resourceUrl: resourceUrl,
},
PollSuspendResumer: &PollSuspendResumer{
SuspendCh: make(chan bool),
ResumeCh: make(chan bool),
},
}
}
The rest of the code defines the methods on the relevant structs, and is available in full on GitHub here.
This provides us with the flexibility that we need to actually fake out each
component of the PollerJob
struct in isolation when we do testing. Each
“mock” component can be re-used and/or re-worked to be more flexible where
needed, allowing us to cover a wide range of possible outcomes from the
components which we depend on.
We can now test Run
in isolation, and without talking to any actual servers.
We simply control what the ServerPoller
returns and verify that what was
written to the Logger
was as we expect by re-implementing those interfaces.
Consequently the test file for PollerJob
looks similar to this.
package main
import (
"errors"
"fmt"
"testing"
"time"
)
type ReadableLogger interface {
Logger
Read() string
}
type MessageReader struct {
Msg string
}
func (mr *MessageReader) Read() string {
return mr.Msg
}
type LastEntryLogger struct {
*MessageReader
}
func (lel *LastEntryLogger) Log(args ...interface{}) {
lel.Msg = fmt.Sprint(args...)
}
type DiscardFirstWriteLogger struct {
*MessageReader
writtenBefore bool
}
func (dfwl *DiscardFirstWriteLogger) Log(args ...interface{}) {
if dfwl.writtenBefore {
dfwl.Msg = fmt.Sprint(args...)
}
dfwl.writtenBefore = true
}
type FakeServerPoller struct {
result string
err error
}
func (fsp FakeServerPoller) PollServer() (string, error) {
return fsp.result, fsp.err
}
func TestPollerJobRunLog(t *testing.T) {
waitBeforeReading := 100 * time.Millisecond
shortInterval := 20 * time.Millisecond
longInterval := 200 * time.Millisecond
testCases := []struct {
p PollerJob
logger ReadableLogger
sp ServerPoller
expectedMsg string
}{
{
p: NewPollerJob("madeup.website", shortInterval),
logger: &LastEntryLogger{&MessageReader{}},
sp: FakeServerPoller{"200 OK", nil},
expectedMsg: "200 OK",
},
{
p: NewPollerJob("down.website", shortInterval),
logger: &LastEntryLogger{&MessageReader{}},
sp: FakeServerPoller{"500 SERVER ERROR", nil},
expectedMsg: "500 SERVER ERROR",
},
{
p: NewPollerJob("error.website", shortInterval),
logger: &LastEntryLogger{&MessageReader{}},
sp: FakeServerPoller{"", errors.New("DNS probe failed")},
expectedMsg: "Error trying to get state: DNS probe failed",
},
{
p: NewPollerJob("some.website", longInterval),
// Discard first write since we want to verify that no
// additional logs get made after the first one (time
// out)
logger: &DiscardFirstWriteLogger{MessageReader: &MessageReader{}},
sp: FakeServerPoller{"200 OK", nil},
expectedMsg: "",
},
}
for _, c := range testCases {
c.p.Logger = c.logger
c.p.ServerPoller = c.sp
go c.p.Run()
time.Sleep(waitBeforeReading)
if c.logger.Read() != c.expectedMsg {
t.Errorf("Expected message did not align with what was written:\n\texpected: %q\n\tactual: %q", c.expectedMsg, c.logger.Read())
}
}
}
Note the creative flexibility that making our own ReadableLogger
interface
for testing, and being able to implement Logger
in a variety of ways,
provides us. Suspend
and Resume
functionality can likewise be tested
meticulously by controlling the ServerPoller
interface component of
JobPoller
.
func TestPollerJobSuspendResume(t *testing.T) {
p := NewPollerJob("foobar.com", 20*time.Millisecond)
waitBeforeReading := 100 * time.Millisecond
expectedLogLine := "200 OK"
normalServerPoller := &FakeServerPoller{expectedLogLine, nil}
logger := &LastEntryLogger{&MessageReader{}}
p.Logger = logger
p.ServerPoller = normalServerPoller
// First start the job / polling
go p.Run()
time.Sleep(waitBeforeReading)
if logger.Read() != expectedLogLine {
t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read())
}
// Then suspend the job
if err := p.Suspend(); err != nil {
t.Errorf("Expected suspend error to be nil but got %q", err)
}
// Fake the log line to detect if poller is still running
newExpectedLogLine := "500 Internal Server Error"
logger.MessageReader.Msg = newExpectedLogLine
// Give it a second to poll if it's going to poll
time.Sleep(waitBeforeReading)
// If this log writes, we know we are polling the server when we're not
// supposed to (job should be suspended).
if logger.Read() != newExpectedLogLine {
t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", newExpectedLogLine, logger.Read())
}
if err := p.Resume(); err != nil {
t.Errorf("Expected resume error to be nil but got %q", err)
}
// Give it a second to poll if it's going to poll
time.Sleep(waitBeforeReading)
if logger.Read() != expectedLogLine {
t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read())
}
}
It certainly might seem like a lot of boilerplate to test a small file, but it will scale well as the size of the code base grows. Mocking out dependent bits in this fashion makes it easier to specify what behavior should be like in error cases or to control flow in the case of tricky concurrency issues.
Due to the practical and creative enhancements that interfaces offer to testability, it is preferred to wrap external dependencies in one and then combine them to create higher-order interfaces wherever possible. As you can hopefully see, even small one-method interfaces are useful to combine into bigger pieces of functionality.
Example #4: Using and faking standard library functionality
The concepts illustrated above are useful for your own programs, but you will also notice that many of the constructs in the Go standard library can be managed in your unit tests in a similar fashion (and indeed they are frequently used in such a manner in the tests for the standard library itself).
Let’s take a look at testing an example HTTP server. It might be tempting to
actually start up the HTTP server in a goroutine and send it the requests that
you expect it to be able to handle directly (e.g. with http.Get
). But that
is far more like an integration test than a proper unit test. Let’s take a
look at a little HTTP server and discuss how to approach testing it.
package main
import (
"fmt"
"log"
"net/http"
)
func mainHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Access-Token")
if token == "magic" {
fmt.Fprintf(w, "You have some magic in you\n")
log.Println("Allowed an access attempt")
} else {
http.Error(w, "You don't have enough magic in you", http.StatusForbidden)
log.Println("Denied an access attempt")
}
}
func main() {
http.HandleFunc("/", mainHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
The above HTTP server listens on :8080/
for requests, and checks if they have
a X-Access-Token
header set. If the token matches our "magic"
value, we
allow the users access and return a HTTP 200 OK status code. Otherwise, we
reject the request with a HTTP 403 Access Forbidden status code. This is a
crude imitation of how some API servers handle authorization. How can we test
it?
As you can see, the mainHandler
function accepts two arguments: a
http.ResponseWriter
(note that it is an interface
, which you can verify by
reading the source of http
or the documentation) and a http.Request
struct
pointer. To test the handler, we could make our own implementation of the
http.ResponseWriter
interface which we could also
read from later, but fortunately the Go authors have already provided a
httptest
package with a ResponseRecorder
struct which is
meant to help with exactly this issue. Having such a module to provide common
testing functionality where needed is a useful and not uncommon pattern.
Given that, we can also create a hand-crafted http.Request
struct by calling
NewRequest
with the expected parameters. We simply have to call Header.Set
on the Request
to set the desired header. We specify in the NewRequest
method that it should be GET
method and not contain any information in the
request body, though we could also test POST
requests and so on if we wanted
to by creating structures for those instead.
The initial test looks like this:
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
func TestMainHandler(t *testing.T) {
rootRequest, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal("Root request error: %s", err)
}
cases := []struct {
w *httptest.ResponseRecorder
r *http.Request
accessTokenHeader string
expectedResponseCode int
expectedResponseBody []byte
}{
{
w: httptest.NewRecorder(),
r: rootRequest,
accessTokenHeader: "magic",
expectedResponseCode: http.StatusOK,
expectedResponseBody: []byte("You have some magic in you\n"),
},
{
w: httptest.NewRecorder(),
r: rootRequest,
accessTokenHeader: "",
expectedResponseCode: http.StatusForbidden,
expectedResponseBody: []byte("You don't have enough magic in you\n"),
},
}
for _, c := range cases {
c.r.Header.Set("X-Access-Token", c.accessTokenHeader)
mainHandler(c.w, c.r)
if c.expectedResponseCode != c.w.Code {
t.Errorf("Status Code didn't match:\n\t%q\n\t%q", c.expectedResponseCode, c.w.Code)
}
if !bytes.Equal(c.expectedResponseBody, c.w.Body.Bytes()) {
t.Errorf("Body didn't match:\n\t%q\n\t%q", string(c.expectedResponseBody), c.w.Body.String())
}
}
}
However, there is one glaring omission of testing functionality we can account
for. We do not check that what was written to the log
is what we expected at
all. How can we do so?
Well, if we examine the source for the standard library’s log
package, we can
see that the log.Println
method directly wraps an instance of the Logger
struct which internally calls the Write
method on a Writer
interface (in
the case of the std
struct which is written to if you invoke log.*
directly, that Writer
is os.Stdout
). Hm, I wonder if there’s any way we
could set that interface to whatever we want so we can verify that what was
written is what we expected?
Naturally, there is a way to do so. We can invoke the log.SetOutput
method
to specify our own custom writer for logging to. In order to pass something in
which we can read from later, we create the Writer
to pass in using
io.Pipe
. This will provide us with a
Reader
that we can use to Read
the subsequent Write
calls made in the
Logger
. We wrap the given PipeReader
in a bufio.Reader
so that we can
easily read line-by-line using a call to bufio.Reader
’s ReadString
method.
Note that in PipeWriter
’s
documentation it says:
Write implements the standard Write interface: it writes data to the pipe, blocking until readers have consumed all the data or the read end is closed.
Therefore, we have to concurrently read from the PipeReader
as the
mainHandler
function is writing to it, so we run that bit of the test in its
own goroutine. In my original version I got this wrong and discovered my error
by using go test
’s -timeout
flag, which will panic any given test if it
stalls for longer than the specified interval.
Put together, it all looks like this:
func TestMainHandler(t *testing.T) {
rootRequest, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal("Root request error: %s", err)
}
cases := []struct {
w *httptest.ResponseRecorder
r *http.Request
accessTokenHeader string
expectedResponseCode int
expectedResponseBody []byte
expectedLogs []string
}{
{
w: httptest.NewRecorder(),
r: rootRequest,
accessTokenHeader: "magic",
expectedResponseCode: http.StatusOK,
expectedResponseBody: []byte("You have some magic in you\n"),
expectedLogs: []string{
"Allowed an access attempt\n",
},
},
{
w: httptest.NewRecorder(),
r: rootRequest,
accessTokenHeader: "",
expectedResponseCode: http.StatusForbidden,
expectedResponseBody: []byte("You don't have enough magic in you\n"),
expectedLogs: []string{
"Denied an access attempt\n",
},
},
}
for _, c := range cases {
logReader, logWriter := io.Pipe()
bufLogReader := bufio.NewReader(logReader)
log.SetOutput(logWriter)
c.r.Header.Set("X-Access-Token", c.accessTokenHeader)
go func() {
for _, expectedLine := range c.expectedLogs {
msg, err := bufLogReader.ReadString('\n')
if err != nil {
t.Errorf("Expected to be able to read from log but got error: %s", err)
}
if !strings.HasSuffix(msg, expectedLine) {
t.Errorf("Log line didn't match suffix:\n\t%q\n\t%q", expectedLine, msg)
}
}
}()
mainHandler(c.w, c.r)
if c.expectedResponseCode != c.w.Code {
t.Errorf("Status Code didn't match:\n\t%q\n\t%q", c.expectedResponseCode, c.w.Code)
}
if !bytes.Equal(c.expectedResponseBody, c.w.Body.Bytes()) {
t.Errorf("Body didn't match:\n\t%q\n\t%q", string(c.expectedResponseBody), c.w.Body.String())
}
}
}
I hope that this example illustrates clearly the value of having well-architected interfaces in the Go standard library as well as in your own code, and how reading the source code of modules upon which you are relying (including the Go standard library, which is meticulously documented) can make your understanding of the code you are working with better as well as ease testing.