Start testing your Go app the right way

When embarking on the journey of acquiring knowledge in any domain, a pristine mental state is paramount.

For individuals relatively unfamiliar with Go but acquainted with languages like JavaScript or Ruby, the reliance on pre-existing frameworks for tasks such as mocking, assertions, and other testing intricacies is a common practice.

It is imperative to relinquish this reliance on external dependencies or frameworks! Testing presented itself as the initial obstacle during my exploration of this extraordinary programming language a couple of years ago, a time characterized by a scarcity of available resources.

My current understanding dictates that achieving success in Go testing necessitates a minimalist approach to dependencies (aligning with the Go philosophy), minimizing reliance on external libraries, and crafting reusable, high-quality code. This presentation of Blake Mizerany’s experiences is a commendable initial step in recalibrating one’s mindset. You’ll encounter compelling arguments both for and against the utilization of external libraries and frameworks, as opposed to adhering to “the Go way.”

While the notion of constructing one’s own testing framework and mocking concepts might appear counterintuitive, it is a more straightforward endeavor than one might perceive. Furthermore, it serves as a valuable starting point for delving into the intricacies of the language. Unlike during my initial learning phase, you have the advantage of this article to guide you through prevalent testing scenarios and introduce techniques I deem best practices for efficient testing and code maintainability.

Do things “the Go Way”, eradicate dependencies on external frameworks.

Table Testing in Go

The fundamental testing unit, widely recognized in the realm of ‘unit testing,’ can be any program component in its simplest form, characterized by accepting an input and producing an output. Let’s examine a straightforward function for which we aim to write tests. While far from perfect or comprehensive, it suffices for demonstration purposes:

avg.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func Avg(nos ...int) int {
	sum := 0
	for _, n := range nos {
		sum += n
	}
	if sum == 0 {
		return 0
	}
	return sum / len(nos)
}

The function depicted above, func Avg(nos ...int), returns either zero or the integer average of a sequence of numbers provided as input. Now, let’s proceed to write a test for this function.

In Go, it is regarded as a best practice to assign a test file the same name as the file containing the code under test, appending the suffix ‘_test’. For instance, given that the aforementioned code resides in a file named ‘avg.go,’ our test file will be named ‘avg_test.go’.

Kindly note that these examples are mere excerpts from actual files, as the package definition and imports have been omitted for brevity.

Below is a test for the ‘Avg’ function:

avg__test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestAvg(t *testing.T) {
	for _, tt := range []struct {
		Nos    []int
		Result int
	}{
		{Nos: []int{2, 4}, Result: 3},
		{Nos: []int{1, 2, 5}, Result: 2},
		{Nos: []int{1}, Result: 1},
		{Nos: []int{}, Result: 0},
		{Nos: []int{2, -2}, Result: 0},
	} {
		if avg := Average(tt.Nos...); avg != tt.Result {
			t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg)
		}
	}
}

Several noteworthy aspects of the function definition warrant attention:

  • The ‘Test’ prefix in the test function name is mandatory for the tool to recognize it as a legitimate test.
  • Conventionally, the latter part of the function name reflects the name of the function or method undergoing testing, in this case, ‘Avg’.
  • We must also pass in the ’testing.T’ structure, which provides control over the test’s execution flow. For a more detailed exposition of this API, please refer to the documentation page.

Let’s now delve into the form and structure of the presented example. A test suite, comprising a series of tests, is executed through the ‘Avg()’ function. Each test encompasses a specific input and its corresponding expected output. In our scenario, each test supplies a slice of integers (‘Nos’) and anticipates a specific return value (‘Result’).

Table testing gets its name from its structure, easily represented by a table with two columns: the input variable and the expected output variable.

Golang Interface Mocking

Among the most remarkable and potent features offered by the Go language is the concept of an interface. Beyond the power and flexibility they bring to program architecture, interfaces empower us with exceptional opportunities to decouple components and subject them to rigorous testing at their interaction points.

An interface is a named collection of methods, but also a variable type.

Let’s envision a hypothetical scenario where we need to read the first N bytes from an io.Reader and return them as a string. The code might resemble the following:

readn.go

1
2
3
4
5
6
7
8
9
// readN reads at most n bytes from r and returns them as a string.
func readN(r io.Reader, n int) (string, error) {
	buf := make([]byte, n)
	m, err := r.Read(buf)
	if err != nil {
		return "", err
	}
	return string(buf[:m]), nil
}

Undoubtedly, the primary objective of testing is to ensure that the ‘readN’ function produces the correct output for various inputs. This can be achieved through table testing. However, there are two additional non-trivial aspects deserving attention:

  • Verification that ‘r.Read’ is invoked with a buffer of size ’n’.
  • Confirmation that ‘r.Read’ returns an error if one is encountered.

To ascertain the buffer size passed to ‘r.Read’ and control the error it returns, we must mock the ‘r’ argument supplied to ‘readN’. Consulting the Go documentation on type Reader reveals the structure of ‘io.Reader’:

1
2
3
type Reader interface {
	   Read(p []byte) (n int, err error)
}

The task seems fairly straightforward. To satisfy ‘io.Reader,’ our mock must possess a ‘Read’ method. Consequently, our ‘ReaderMock’ can be defined as follows:

1
2
3
4
5
6
7
type ReaderMock struct {
	ReadMock func([]byte) (int, error)
}

func (m ReaderMock) Read(p []byte) (int, error) {
	return m.ReadMock(p)
}

Let’s briefly analyze the above code. Any instance of ‘ReaderMock’ inherently fulfills the ‘io.Reader’ interface by implementing the requisite ‘Read’ method. Our mock also incorporates the ‘ReadMock’ field, enabling us to precisely define the mocked method’s behavior, thereby simplifying the dynamic instantiation of our desired actions.

A valuable memory-efficient technique for runtime interface satisfaction involves introducing the following code snippet:

1
var _ io.Reader = (*MockReader)(nil)

This assertion check, without allocating memory, ensures correct interface implementation at compile time, before the program encounters any functionality utilizing it. While optional, this trick proves beneficial.

Moving forward, let’s craft our first test, where ‘r.Read’ is called with a buffer of size ’n’. Leveraging our ‘ReaderMock,’ we proceed as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func TestReadN_bufSize(t *testing.T) {
	total := 0
	mr := &MockReader{func(b []byte) (int, error) {
		total = len(b)
		return 0, nil
	}}
	readN(mr, 5)
	if total != 5 {
		t.Fatalf("expected 5, got %d", total)
	}
}

As evident above, we’ve defined the behavior of our “fake” ‘io.Reader’s’ ‘Read’ function using a scoped variable. This variable can later be employed to validate our test. A rather elegant solution.

Let’s now examine the second scenario requiring our attention, where we need to mock ‘Read’ to return an error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func TestReadN_error(t *testing.T) {
	expect := errors.New("some non-nil error")
	mr := &MockReader{func(b []byte) (int, error) {
		return 0, expect
	}}
	_, err := readN(mr, 5)
	if err != expect {
		t.Fatal("expected error")
	}
}

In this testing scenario, any invocation of ‘mr.Read’ (our mocked Reader) will consistently return the predefined error. Thus, we can safely assume that the correct behavior of ‘readN’ will mirror this.

Function Mocking with Go

The need to mock a function arises infrequently, as structures and interfaces often prove more suitable. Their inherent controllability makes them preferable. However, there are occasional instances where function mocking becomes necessary, and I frequently observe confusion surrounding this topic. Some have even inquired about mocking entities like ’log.Println’. While testing input to ’log.Println’ is seldom required, we’ll use this as an opportunity for demonstration.

Consider the simple ‘if’ statement below, which logs output based on the value of ’n’:

1
2
3
4
5
6
7
func printSize(n int) {
	if n < 10 {
		log.Println("SMALL")
	} else {
		log.Println("LARGE")
	}
}

In this rather absurd example, we aim to specifically test that ’log.Println’ is called with the correct values. To mock this function, we must first encapsulate it within our own function:

1
2
3
var show = func(v ...interface{}) {
	log.Println(v...)
}

Declaring the function as a variable enables us to overwrite it within our tests and assign our desired behavior. Implicitly, references to ’log.Println’ are replaced with ‘show,’ modifying our program as follows:

1
2
3
4
5
6
7
func printSize(n int) {
	if n < 10 {
		show("SMALL")
	} else {
		show("LARGE")
	}
}

Now, we can proceed with testing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func TestPrintSize(t *testing.T) {
	var got string
	oldShow := show
	show = func(v ...interface{}) {
		if len(v) != 1 {
			t.Fatalf("expected show to be called with 1 param, got %d", len(v))
		}
		var ok bool
		got, ok = v[0].(string)
		if !ok {
			t.Fatal("expected show to be called with a string")
		}
	}

	for _, tt := range []struct{
		N int
		Out string
	}{
		{2, "SMALL"},
		{3, "SMALL"},
		{9, "SMALL"},
		{10, "LARGE"},
		{11, "LARGE"},
		{100, "LARGE"},
	} {
		got = ""
		printSize(tt.N)
		if got != tt.Out {
			t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got)
		}
	}

	// careful though, we must not forget to restore it to its original value
	// before finishing the test, or it might interfere with other tests in our
	// suite, giving us unexpected and hard to trace behavior.
	show = oldShow
}

The key takeaway shouldn’t be solely focused on mocking ’log.Println’. Instead, it should be understood that in those rare scenarios where mocking a package-level function is genuinely necessary, the only known method (to my knowledge) is to declare it as a package-level variable. This grants us control over its value.

However, if circumstances necessitate mocking entities like ’log.Println,’ a significantly more elegant solution can be achieved by utilizing a custom logger.

Go Template Rendering Tests

Another common scenario involves testing whether the output of a rendered template aligns with expectations. Let’s consider a GET request to ‘http://localhost:3999/welcome?name=Frank’, which returns the following body:

1
2
3
4
5
6
7
8
<html>
	<head><title>Welcome page</title></head>
	<body>
		<h1 class="header-name">
			Welcome <span class="name">Frank</span>!
		</h1>
	</body>
</html>

In case it wasn’t readily apparent, the correspondence between the ’name’ query parameter and the content of the ‘span’ element classed as “name” is no coincidence. In this case, the obvious test would involve verifying the consistency of this behavior across multiple outputs. I found the GoQuery library to be invaluable in this regard.

GoQuery uses a jQuery-like API to query an HTML structure, which is indispensable for testing the validity of the markup output of your programs.

We can now formulate our test as follows:

welcome__test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestWelcome_name(t *testing.T) {
	resp, err := http.Get("http://localhost:3999/welcome?name=Frank")
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	doc, err := goquery.NewDocumentFromResponse(resp)
	if err != nil {
		t.Fatal(err)
	}
	if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" {
		t.Fatalf("expected markup to contain 'Frank', got '%s'", v)
	}
}

As a preliminary step, we confirm that the response code is 200/OK before proceeding.

It’s reasonable to assume that the remaining code snippet is self-explanatory: we retrieve the URL using the ‘http’ package and construct a new goquery-compatible document from the response. This document then allows us to query the returned DOM. We assert that the ‘span.name’ element within ‘h1.header-name’ encapsulates the text ‘Frank’.

Testing JSON APIs

Go finds frequent application in building APIs. Therefore, let’s conclude by exploring some high-level approaches to testing JSON APIs.

Suppose the aforementioned endpoint returned JSON instead of HTML. In this case, we would expect the response body from ‘http://localhost:3999/welcome.json?name=Frank’ to resemble the following:

1
{"Salutation": "Hello Frank!"}

Asserting JSON responses, as one might have surmised, bears a striking resemblance to asserting template responses. The primary distinction is the absence of external libraries or dependencies, as Go’s standard libraries suffice. Here’s our test confirming the return of the correct JSON for the given parameters:

welcome__test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestWelcome_name_JSON(t *testing.T) {
	resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank")
	if err != nil {
		t.Fatal(err)
	}
	if resp.StatusCode != 200 {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	var dst struct{ Salutation string }
	if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil {
		t.Fatal(err)
	}
	if dst.Salutation != "Hello Frank!" {
		t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation)
	}
}

Should anything other than the structure we decode against be returned, ‘json.NewDecoder’ would instead produce an error, causing the test to fail. Given the successful decoding of the response against the structure, we proceed to verify that the field’s contents match our expectations—in this instance, “Hello Frank!”.

Setup & Teardown

Testing in Go is undeniably straightforward. However, both the JSON test and the preceding template rendering test share a common flaw: they assume a running server, introducing an unreliable dependency. Moreover, interacting with a “live” server is generally not advisable.

It’s never a good idea to test against “live” data on a “live” production server; spin up local or development copies so there’s no damage done with things go horribly wrong.

Fortunately, Go offers the httptest package for creating test servers. These servers operate independently of our main server, ensuring that testing remains isolated from production.

In such cases, it’s considered best practice to define generic ‘setup’ and ’teardown’ functions to be invoked by all tests requiring a running server. Embracing this safer pattern would result in tests resembling the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func setup() *httptest.Server {
	return httptest.NewServer(app.Handler())
}

func teardown(s *httptest.Server) {
	s.Close()
}

func TestWelcome_name(t *testing.T) {
	srv := setup()

	url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL)
	resp, err := http.Get(url)
	// verify errors & run assertions as usual

	teardown(srv)
}

Note the ‘app.Handler()’ reference. This function, adhering to best practices, returns the application’s http.Handler, which can instantiate either your production server or a test server, depending on the context.

Conclusion

Testing in Go provides an invaluable opportunity to adopt the external perspective of your program, stepping into the shoes of your visitors or, more commonly, your API consumers. It allows us to ensure the delivery of both high-quality code and a satisfactory user experience.

Whenever uncertainties arise regarding the more intricate functionalities within your code, testing emerges as a reassuring tool, guaranteeing that the individual components continue to function harmoniously even when modifications are introduced to larger systems.

I trust this article has proven beneficial. Feel free to share any additional testing tricks you may have encountered.

Licensed under CC BY-NC-SA 4.0