A unit test for the JSON function
Let's write some unit tests as a way of getting familiar with Go's built-in testing library. Looking in server.go
, we can find the first function that isn't main
and write a test for it.
So let us write a test for the JSON
function, starting with making a failing test and running it.
We start by creating a server_test.go
file in the same directory as server.go
.
Go will automatically know this is a test file because of the ending _test.go
, and naming it starting with server
lets us know that we'll be writing tests for server.go
in it.
package main
import "testing"
func TestJSON(t *testing.T) {
t.Fatal("not implemented")
}
In the same directory as the test file we run our new test with go test
and we will see our expected failure from the t.Fatal
:
$ go test
--- FAIL: TestJSON (0.00s)
server_test.go:6: not implemented
FAIL
exit status 1
FAIL github.com/fullstackio/reliable-go/kv-store 0.009s
Great! Now in order to replace that with something which will actually test the function, let's think about what the function is doing. It encodes arbitrary data and then writes it to a http.ResponseWriter
. So if we want to test it, we need to
supply the data
know what its encoded form looks like
check that it was written to the
http.ResponseWriter
Because we are currently only using JSON
to encode data of the type map[string]string
, that seems like a good type to test. So - let's test the message "hello world" in a map form. Add these lines at the top of the TestJSON
function:
in := map[string]string{"hello": "world"}
out := `{"hello":"world"}`
I'm using the variable names in
for the input we will pass to JSON
, and out
for the output we expect to see from it.
This is great and should be a good test, but we have an issue: JSON
takes an input but it doesn't return an output. So we write to the response writer, but have no way of checking that data.
Luckily, Go's standard library has something that will help us: the httptest
library - and it is lovely!
httptest.ResponseRecorder
is a struct that implements the http.ResponseWriter
interface but also allows you to check what changes were made to it afterwards. If we use one of these, we can pass it with our message into JSON
and then check the results after.
Use it and call JSON
with it like this:
recorder := httptest.NewRecorder()
JSON(recorder, in)
JSON
will now have written to our ResponseRecorder
, giving us the ability to check those values.
For now let us only test that the body written to the recorder is the same as our input message. We can access it with its Result()
method which gives us the http.Response
we normally get when making a request.
From that we can read the entire body, convert it to a string, and compare it with our input.
response := recorder.Result()
defer response.Body.Close()
got, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("Error reading response body: %s", err)
}
if string(got) != out {
t.Errorf("Got %s, expected %s", string(got), out)
}
Note that we use t.Fatalf
here if we fail to read the body to indicate that our test has failed to run and we shouldn't continue. We always expect to be able to read it here, so if we can not, something is seriously wrong.
We use t.Errorf
for our actual test case to show that it has failed, but the other tests should continue.
Putting it all together looks like this:
package main
import (
"io"
"net/http/httptest"
"testing"
)
func TestJSON(t *testing.T) {
in := map[string]string{"hello": "world"}
out := `{"hello":"world"}`
recorder := httptest.NewRecorder()
JSON(recorder, in)
response := recorder.Result()
defer response.Body.Close()
got, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("Error reading response body: %s", err)
}
if string(got) != out {
t.Errorf("Got %s, expected %s", string(got), out)
}
}
Enhancing the JSON unit test with tables#
Our test is currently fairly simple. It only checks the body of the response, not any headers, and it only tests one message and one data type. Luckily, Go has a common pattern we can use to scale this up: table-driven tests!
Instead of having only one message to test, we can make a slice of them, with both our input and expected output. We can then use a for
loop to run all of our tests one after the other. Adding a new case will then be as simple as adding another element to the slice - we'll do that shortly by adding the {"hello":"tables"}
message.
Before we do that, we need to convert our first test case into a table of test cases, including both our input and our expected output. Our input is a map of string to string, and our output is a string. So we need to create a slice of these inputs and outputs. Fortunately, we can use an anonymous struct, which is a struct that is defined without a name. It will let us create our desired slice and immediately create values to fill it. Also, because it is defined only in this one test case, future test cases can use their own anonymous structs if they have different inputs or outputs. The syntax looks slightly weird, because we are used to having our struct definition separate from instantiation. Combined with the fact that this is also a struct means there are a whole lot of curly braces. But it gives us exactly what we're looking for, which is great.
testCases := []struct {
in map[string]string
out string
}{
{map[string]string{"hello": "world"}, `{"hello":"world"}`},
}
In this section of code we are not defining a type for our struct but defining the struct inline. It's not necessarily the best practice, but so long as we are only using this structure here it should be fine. If we find ourselves wanting to use this in more than one test case we can convert it to a regular struct and reuse it.
With this struct, we can replace our in
and out
variables with test.in
and test.out
, which is our range variable over our testCases
slice. We also create a loop around the main section of our test to loop through it with the one case we have provided. The loop encompasses everything from creating the recorder to the last if
statement checking our output.
for _, test := range testCases {
recorder := httptest.NewRecorder()
JSON(recorder, test.in)
response := recorder.Result()
defer response.Body.Close()
got, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("Error reading response body: %s", err)
}
if string(got) != test.out {
t.Errorf("Got %s, expected %s", string(got), test.out)
}
}
Now that we've converted our first test into a table-driven test, let us make sure it still works! go test
will do the job.
This page is a preview of Reliable Webservers with Go