Chapter 9. Testing and benchmarking

In this chapter

  • Writing unit tests to validate your code
  • Mocking HTTP-based requests and responses using httptest
  • Documenting your packages with example code
  • Examining performance with benchmarks

Testing your code is not something that you should wait to do until after you’re finished developing your program. With Go’s testing framework, unit testing and benchmarking can happen during the development process. Just like the go build command, there’s a go test command to execute explicit test code that you write. All you need to do is follow a few guidelines, and you can integrate tests into your project and continuous integration systems seamlessly.

9.1. Unit testing

A unit test is a function that tests a specific piece or set of code from a package or program. The job of the test is to determine whether the code in question is working as expected for a given scenario. One scenario may be a positive-path test, where the test is making sure the normal execution of the code doesn’t produce an error. This could be a test that validates that the code can insert a job record into the database successfully.

Other unit tests may test negative-path scenarios to make sure the code produces not only an error, but the expected one. This could be a test that makes a query against a database where no results are found, or performs an invalid update against a database. In both cases, the test would validate that the error is reported and the correct error context is provided. In the end, the code you write must be predictable no matter how it’s called or executed.

There are several ways in Go to write unit tests. Basic tests test a specific piece of code for a single set of parameters and result. Table tests also test a specific piece of code, but the test validates itself against multiple parameters and results. There are also ways to mock external resources that the test code needs, such as databases or web servers. This helps to simulate the existence of these resources during testing without the need for them to be available. Finally, when building your own web services, there are ways to test calls coming in to the service without ever needing to run the service itself.

9.1.1. Basic unit test

Let’s start with an example of a unit test.

Listing 9.1. listing01_test.go
01 // Sample test to show how to write a basic unit test.
02 package listing01
03
04 import (
05     "net/http"
06     "testing"
07 )
08
09 const checkMark = "u2713"
10 const ballotX = "u2717"
11
12 // TestDownload validates the http Get function can download content.
13 func TestDownload(t *testing.T) {
14     url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
15     statusCode := 200
16
17     t.Log("Given the need to test downloading content.")
18     {
19         t.Logf("	When checking "%s" for status code "%d"",
20             url, statusCode)
21         {

22             resp, err := http.Get(url)
23             if err != nil {
24                 t.Fatal("		Should be able to make the Get call.",
25                     ballotX, err)
26             }
27             t.Log("		Should be able to make the Get call.",
28                 checkMark)
29
30             defer resp.Body.Close()
31
32             if resp.StatusCode == statusCode {
33                 t.Logf("		Should receive a "%d" status. %v",
34                     statusCode, checkMark)
35             } else {
36                 t.Errorf("		Should receive a "%d" status. %v %v",
37                     statusCode, ballotX, resp.StatusCode)
38             }
39         }
40     }
41 }

Listing 9.1 shows a unit test that’s testing the Get function from the http package. It’s testing that the goinggo.net RSS feed can be downloaded properly from the web. When we run this test by calling go test -v, where -v means provide verbose output, we get the test results shown in figure 9.1.

Figure 9.1. Output from the basic unit test

A lot of little things are happening in this example to make this test work and display the results as it does. It all starts with the name of the test file. If you look at the top of listing 9.1, you’ll see that the name of the test file is listing01_test.go. The Go testing tool will only look at files that end in _test.go. If you forget to follow this convention, running go test inside of a package may report that there are no test files. Once the testing tool finds a testing file, it then looks for testing functions to run.

Let’s take a closer look at the code in the listing01_test.go test file.

Listing 9.2. listing01_test.go: lines 01–10
01 // Sample test to show how to write a basic unit test.
02 package listing01
03

04 import (
05     "net/http"
06     "testing"
07 )
08
09 const checkMark = "u2713"
10 const ballotX = "u2717"

In listing 9.2, you can see the import of the testing package on line 06. The testing package provides the support we need from the testing framework to report the output and status of any test. Lines 09 and 10 provide two constants that contain the characters for the check mark and X mark that will be used when writing test output.

Next, let’s look at the declaration of the test function.

Listing 9.3. listing01_test.go: lines 12–13
12 // TestDownload validates the http Get function can download content.
13 func TestDownload(t *testing.T) {

The name of the test function is TestDownload, and you can see it on line 13 in listing 9.3. A test function must be an exported function that begins with the word Test. Not only must the function start with the word Test, it must have a signature that accepts a pointer of type testing.T and returns no value. If we don’t follow these conventions, the testing framework won’t recognize the function as a test function and none of the tooling will work against it.

The pointer of type testing.T is important. It provides the mechanism for reporting the output and status of each test. There’s no one standard for formatting the output of your tests. I like the test output to read well, which does follow the Go idioms for writing documentation. For me, the testing output is documentation for the code. The test output should document why the test exists, what’s being tested, and the result of the test in clear complete sentences that are easy to read. Let’s see how I accomplish this as we review more of the code.

Listing 9.4. listing01_test.go: lines 14–18
14     url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
15     statusCode := 200
16
17     t.Log("Given the need to test downloading content.")
18     {

You see on lines 14 and 15 in listing 9.4 that two variables are declared and initialized. These variables contain the URL we want to test and the status we expect back from the response. On line 17 the t.Log method is used to write a message to the test output. There’s also a format version of this method called t.Logf. If the verbose option (-v) isn’t used when calling go test, we won’t see any test output unless the test fails.

Each test function should state why the test exists by explaining the given need of the test. For this test, the given need is to test downloading content. After declaring the given need of the test, the test should then state when the code being tested would execute, and how.

Listing 9.5. listing01_test.go: lines 19–21
19         t.Logf("	When checking "%s" for status code "%d"",
20             url, statusCode)
21         {

You see the when clause on line 19 in listing 9.5. It states specifically the values for the test. Next, let’s look at the code being tested using these values.

Listing 9.6. listing01_test.go: lines 22–30
22             resp, err := http.Get(url)
23             if err != nil {
24                 t.Fatal("		Should be able to make the Get call.",
25                     ballotX, err)
26             }
27             t.Log("		Should be able to make the Get call.",
28                 checkMark)
29
30             defer resp.Body.Close()

The code in listing 9.6 uses the Get function from the http package to make a request to the goinggo.net web server to pull down the RSS feed file for the blog. After the Get call returns, the error value is checked to see if the call was successful or not. In either case, we state what the result of the test should be. If the call failed, we write an X as well to the test output along with the error. If the test succeeded, we write a check mark.

If the call to Get does fail, the use of the t.Fatal method on line 24 lets the testing framework know this unit test has failed. The t.Fatal method not only reports the unit test has failed, but also writes a message to the test output and then stops the execution of this particular test function. If there are other test functions that haven’t run yet, they’ll be executed. A formatted version of this method is named t.Fatalf.

When we need to report the test has failed but don’t want to stop the execution of the particular test function, we can use the t.Error family of methods.

Listing 9.7. listing01_test.go: lines 32–41
32             if resp.StatusCode == statusCode {
33                 t.Logf("		Should receive a "%d" status. %v",
34                     statusCode, checkMark)
35             } else {
36                 t.Errorf("		Should receive a "%d" status. %v %v",
37                     statusCode, ballotX, resp.StatusCode)
38             }
39         }
40     }
41 }

One line 32 in listing 9.7, the status code from the response is compared with the status code we expect to receive. Again, we state what the result of the test should be. If the status codes match, then we use the t.Logf method; otherwise, we use the t.Errorf method. Since the t.Errorf method doesn’t stop the execution of the test function, if there were more tests to conduct after line 38, the unit test would continue to be executed. If the t.Fatal or t.Error functions aren’t called by a test function, the test will be considered as passing.

If you look at the output of the test one more time (see figure 9.2), you can see how it all comes together.

Figure 9.2. Output from the basic unit test

In figure 9.2 you see the complete documentation for the test. Given the need to download content, when checking the URL for the statusCode (which is cut off in the figure), we should be able to make the call and should receive a status of 200. The testing output is clear, descriptive, and informative. We know what unit test was run, that it passed, and how long it took: 435 milliseconds.

9.1.2. Table tests

When you’re testing code that can accept a set of different parameters with different results, a table test should be used. A table test is like a basic unit test except it maintains a table of different values and results. The different values are iterated over and run through the test code. With each iteration, the results are checked. This helps to leverage a single test function to test a set of different values and conditions. Let’s look at an example table test.

Listing 9.8. listing08_test.go
01 // Sample test to show how to write a basic unit table test.
02 package listing08
03
04 import (
05     "net/http"
06     "testing"
07 )
08
09 const checkMark = "u2713"
10 const ballotX = "u2717"
11
12 // TestDownload validates the http Get function can download

13 //  content and handles different status conditions properly.
14 func TestDownload(t *testing.T) {
15     var urls = []struct {
16         url        string
17         statusCode int
18     }{
19          {
20              "http://www.goinggo.net/feeds/posts/default?alt=rss",
21              http.StatusOK,
22          },
23          {
24              "http://rss.cnn.com/rss/cnn_topstbadurl.rss",
25              http.StatusNotFound,
26          },
27     }
28
29     t.Log("Given the need to test downloading different content.")
30     {
31         for _, u := range urls {
32             t.Logf("	When checking "%s" for status code "%d"",
33                 u.url, u.statusCode)
34             {
35                 resp, err := http.Get(u.url)
36                 if err != nil {
37                     t.Fatal("		Should be able to Get the url.",
38                         ballotX, err)
39                 }
40                 t.Log("		Should be able to Get the url",
41                     checkMark)
42
43                 defer resp.Body.Close()
44
45                 if resp.StatusCode == u.statusCode {
46                     t.Logf("		Should have a "%d" status. %v",
47                         u.statusCode, checkMark)
48                 } else {
49                     t.Errorf("		Should have a "%d" status %v %v",
50                         u.statusCode, ballotX, resp.StatusCode)
51                 }
52             }
53         }
54     }
55 }

In listing 9.8, we’ve taken the basic unit test and converted it to a table test. Now we can use a single test function to test different URLs and status codes against the http.Get function. We don’t need to create a new test function for each URL and status code we want to test. Let’s look at the changes.

Listing 9.9. listing08_test.go: lines 12–27
12 // TestDownload validates the http Get function can download
13 //  content and handles different status conditions properly.
14 func TestDownload(t *testing.T) {

15     var urls = []struct {
16         url        string
17         statusCode int
18     }{
19          {
20              "http://www.goinggo.net/feeds/posts/default?alt=rss",
21              http.StatusOK,
22          },
23          {
24              "http://rss.cnn.com/rss/cnn_topstbadurl.rss",
25              http.StatusNotFound,
26          },
27     }

In listing 9.9 you see the same test function, TestDownload, accepting a pointer of type testing.T. But this version of TestDownload is slightly different. On lines 15 through 27, you see the implementation of the table. The first field of the table is a URL to a given resource on the internet, and the second field is the status we expect to receive when we make the request for the resource.

Currently, we’ve configured the table with two values. The first value is the goinggo.net URL with a status of OK, and the second value is a different URL with a status of NotFound. The second URL has been misspelled to cause the server to return a NotFound error. When we run this test, we get the test output shown in figure 9.3.

Figure 9.3. Output from the table test

The output in figure 9.3 shows how the table of values is iterated over and used to conduct the test. The output looks the same as the basic unit test except we tested two different URLs this time. Once again, the test passes.

Let’s look at the changes we made to make the table test work.

Listing 9.10. listing08_test.go: lines 29–34
29     t.Log("Given the need to test downloading different content.")
30     {
31         for _, u := range urls {
32             t.Logf("	When checking "%s" for status code "%d"",
33                 u.url, u.statusCode)
34             {

The for range loop on line 31 in listing 9.10 allows the test to iterate over the table and run the test code for each different URL. The original code from the basic unit test is the same except for the use of the table values.

Listing 9.11. listing08_test.go: lines 35–55
35                 resp, err := http.Get(u.url)
36                 if err != nil {
37                     t.Fatal("		Should be able to Get the url.",
38                         ballotX, err)
39                 }
40                 t.Log("		Should be able to Get the url",
41                     checkMark)
42
43                 defer resp.Body.Close()
44
45                 if resp.StatusCode == u.statusCode {
46                     t.Logf("		Should have a "%d" status. %v",
47                         u.statusCode, checkMark)
48                 } else {
49                     t.Errorf("		Should have a "%d" status %v %v",
50                         u.statusCode, ballotX, resp.StatusCode)
51                 }
52             }
53         }
54     }
55 }

Listing 9.11 shows how, on line 35, the code uses the u.url field for the URL to call. On line 45 the u.statusCode field is used to compare the actual status code from the response. In the future, new URLs and status codes can be added to the table and the core of the test doesn’t need to change.

9.1.3. Mocking calls

The unit tests we wrote are great, but they do have a couple of flaws. First, they require access to the internet in order for the tests to run successfully. Figure 9.4 shows what happens when we run the basic unit test again without an internet connection—the test fails.

Figure 9.4. Failed test due to having no internet connection

You shouldn’t always assume the computer you have to run tests on can access the internet. Also, it’s not good practice to have tests depend on servers that you don’t own or operate. Both of these things can have a great impact on any automation you put into place for continuous integration and deployment. Suddenly you can’t deploy a new build because you lost your access to the outside world. If the tests fail, you can’t deploy.

To fix this situation, the standard library has a package called httptest that will let you mock HTTP-based web calls. Mocking is a technique many developers use to simulate access to resources that won’t be available when tests are run. The httptest package provides you with the ability to mock requests and responses from web resources on the internet. By mocking the http.Get response in our unit test, we can solve the problem we saw in figure 9.4. No longer will our test fail because we don’t have an internet connection. Yet the test can validate that our http.Get call works and handles the expected response. Let’s take the basic unit test and change it to mock a call to the goinggo.net RSS feed.

Listing 9.12. listing12_test.go: lines 01–41
01 // Sample test to show how to mock an HTTP GET call internally.
02 // Differs slightly from the book to show more.
03 package listing12
04
05 import (
06     "encoding/xml"
07     "fmt"
08     "net/http"
09     "net/http/httptest"
10     "testing"
11 )
12
13 const checkMark = "u2713"
14 const ballotX = "u2717"
15
16 // feed is mocking the XML document we except to receive.
17 var feed = `<?xml version="1.0" encoding="UTF-8"?>
18 <rss>
19 <channel>
20     <title>Going Go Programming</title>
21     <description>Golang : https://github.com/goinggo</description>
22     <link>http://www.goinggo.net/</link>
23     <item>
24         <pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
25         <title>Object Oriented Programming Mechanics</title>
26         <description>Go is an object oriented language.</description>
27         <link>http://www.goinggo.net/2015/03/object-oriented</link>
28     </item>
29 </channel>
30 </rss>`
31
32 // mockServer returns a pointer to a server to handle the get call.
33 func mockServer() *httptest.Server {

34     f := func(w http.ResponseWriter, r *http.Request) {
35         w.WriteHeader(200)
36         w.Header().Set("Content-Type", "application/xml")
37         fmt.Fprintln(w, feed)
38     }
39
40     return httptest.NewServer(http.HandlerFunc(f))
41 }

Listing 9.12 shows how we can mock a call to the goinggo.net website to simulate the downloading of the RSS feed. On line 17 a package-level variable named feed is declared and initialized with a literal string that represents the RSS XML document we’ll receive from our mock server call. It’s a small snippet of the actual RSS feed document and is enough to conduct our test. On line 32 we have the declaration of a function named mockServer that leverages the support inside the httptest package to simulate a call to a real server on the internet.

Listing 9.13. listing12_test.go: lines 32–40
32 func mockServer() *httptest.Server {
33     f := func(w http.ResponseWriter, r *http.Request) {
34         w.WriteHeader(200)
35         w.Header().Set("Content-Type", "application/xml")
36         fmt.Fprintln(w, feed)
37     }
38
39     return httptest.NewServer(http.HandlerFunc(f))
40 }

The mockServer function in listing 9.13 is declared to return a pointer of type httptest.Server. The httptest.Server value is the key to making all of this work. The code starts out with declaring an anonymous function that has the same signature as the http.HandlerFunc function type.

Listing 9.14. golang.org/pkg/net/http/#HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)

The HandlerFunc type is an adapter to allow the use of ordinary
functions as HTTP handlers. If f is a function with the appropriate
signature, HandlerFunc(f) is a Handler object that calls f

This makes the anonymous function a handler function. Once the handler function is declared, then on line 39 it’s used as a parameter for the httptest.NewServer function call to create our mock server. Then the mock server is returned via a pointer on line 39.

We’ll be able to use this mock server with our http.Get call to simulate hitting the goinggo.net web server. When the http.Get call is made, the handler function is actually executed and used to mock the request and response. On line 34 the handler function first sets the status code; then, on line 35, the content type is set; and finally, on line 36, the XML string named feed that represents the response is returned as the response body.

Now, let’s look at how the mock server is integrated into the basic unit test and how the http.Get call is able to use it.

Listing 9.15. listing12_test.go: lines 43–74
43 // TestDownload validates the http Get function can download content
44 // and the content can be unmarshaled and clean.
45 func TestDownload(t *testing.T) {
46     statusCode := http.StatusOK
47
48     server := mockServer()
49     defer server.Close()
50
51     t.Log("Given the need to test downloading content.")
52     {
53         t.Logf("	When checking "%s" for status code "%d"",
54             server.URL, statusCode)
55         {
56             resp, err := http.Get(server.URL)
57             if err != nil {
58                 t.Fatal("		Should be able to make the Get call.",
59                     ballotX, err)
60             }
61             t.Log("		Should be able to make the Get call.",
62                 checkMark)
63
64             defer resp.Body.Close()
65
66             if resp.StatusCode != statusCode {
67                 t.Fatalf("		Should receive a "%d" status. %v %v",
68                     statusCode, ballotX, resp.StatusCode)
69             }
70             t.Logf("		Should receive a "%d" status. %v",
71                statusCode, checkMark)
72         }
73     }
74 }

In listing 9.15 you see the TestDownload function once more, but this time it’s using the mock server. On lines 48 and 49 a call to the mockServer function is made, and a call to the Close method is deferred for when the test function returns. After that, the test code looks identical to the basic unit test except for one thing.

Listing 9.16. listing12_test.go: line 56
56             resp, err := http.Get(server.URL)

This time the URL to call is provided to by the httptest.Server value. When we use the URL provided by the mocking server, the http.Get call runs as expected. The http.Get call has no idea it’s not making a call over the internet. The call is made and our handler function is executed underneath, resulting in a response of our RSS XML document and a status of http.StatusOK.

When we run the test now without an internet connection, we see the test runs and passes, as shown in figure 9.5. This figure shows how the test is passing again. If you look at the URL used to make the call, you can see it’s using the localhost address with port number 52065. That port number will change every time we run the test. The http package, in conjunction with the httptest package and our mock server, knows to route that URL to our handler function. Now, we can test our calls to the goinggo.net RSS feed without ever hitting the actual server.

Figure 9.5. Successful test without having an internet connection

9.1.4. Testing endpoints

If you’re building a web API, you’ll want to test all of your endpoints without the need to start the web service. The httptest package provides a facility for doing just this. Let’s take a look at a sample web service that implements a single endpoint, and then you can see how to write a unit test that mocks an actual call.

Listing 9.17. listing17.go
01 // This sample code implement a simple web service.
02 package main
03
04 import (
05     "log"
06     "net/http"
07
08     "github.com/goinaction/code/chapter9/listing17/handlers"
09 )
10
11 // main is the entry point for the application.
12 func main() {
13     handlers.Routes()
14
15     log.Println("listener : Started : Listening on :4000")
16     http.ListenAndServe(":4000", nil)
17 }

Listing 9.17 shows the code file for the entry point of the web service. Inside the main function on line 13, the code calls the Routes function from the internal handlers package. This function sets up the routes for the different endpoints the web service is hosting. On lines 15 and 16 the main function displays the port the service is listening on and starts the web service, waiting for requests.

Now, let’s look at the code for the handlers package.

Listing 9.18. handlers/handlers.go
01 // Package handlers provides the endpoints for the web service.
02 package handlers
03
04 import (
05     "encoding/json"
06     "net/http"
07 )
08
09 // Routes sets the routes for the web service.
10 func Routes() {
11     http.HandleFunc("/sendjson", SendJSON)
12 }
13
14 // SendJSON returns a simple JSON document.
15 func SendJSON(rw http.ResponseWriter, r *http.Request) {
16     u := struct {
17         Name  string
18         Email string
19     }{
20         Name:  "Bill",
21         Email: "[email protected]",
22     }
23
24     rw.Header().Set("Content-Type", "application/json")
25     rw.WriteHeader(200)
26     json.NewEncoder(rw).Encode(&u)
27 }

The code for the handlers package in listing 9.18 provides the implementation of the handler function and sets up the routes for the web service. On line 10 you see the Routes function, which uses the default http.ServeMux from inside the http package to configure the routing between the URLs and the corresponding handler code. On line 11 we bind the /sendjson endpoint to the SendJSON function.

Starting on line 15, we have the implementation of the SendJSON function. The function has the same signature as the http.HandlerFunc function type that you saw in listing 9.14. On line 16 an anonymous struct type is declared, and a variable named u is created with some values. On lines 24 and 25 the content type and status code for the response is set. Finally, on line 26 the u value is encoded into a JSON document and sent back to the client.

If we build the web service and start the server, we see the JSON document served up, as in figures 9.6 and 9.7.

Figure 9.6. Running the web service

Figure 9.7. Web service serving up the JSON document

Now that we have a functioning web service with an endpoint, we can write a unit test to test the endpoint.

Listing 9.19. handlers/handlers_test.go
01 // Sample test to show how to test the execution of an
02 // internal endpoint.
03 package handlers_test
04
05 import (
06     "encoding/json"
07     "net/http"
08     "net/http/httptest"
09     "testing"
10
11     "github.com/goinaction/code/chapter9/listing17/handlers"
12 )
13
14 const checkMark = "u2713"
15 const ballotX = "u2717"
16
17 func init() {
18     handlers.Routes()
19 }
20
21 // TestSendJSON testing the sendjson internal endpoint.
22 func TestSendJSON(t *testing.T) {
23     t.Log("Given the need to test the SendJSON endpoint.")
24     {
25         req, err := http.NewRequest("GET", "/sendjson", nil)
26         if err != nil {
27             t.Fatal("	Should be able to create a request.",
28                 ballotX, err)
29         }
30         t.Log("	Should be able to create a request.",
31             checkMark)
32
33         rw := httptest.NewRecorder()
34         http.DefaultServeMux.ServeHTTP(rw, req)
35
36         if rw.Code != 200 {
37             t.Fatal("	Should receive "200"", ballotX, rw.Code)
38         }
39         t.Log("	Should receive "200"", checkMark)
40

41         u := struct {
42             Name  string
43             Email string
44         }{}
45
46         if err := json.NewDecoder(rw.Body).Decode(&u); err != nil {
47             t.Fatal("	Should decode the response.", ballotX)
48         }
49         t.Log("	Should decode the response.", checkMark)
50
51         if u.Name == "Bill" {
52           t.Log("	Should have a Name.", checkMark)
53         } else {
54           t.Error("	Should have a Name.", ballotX, u.Name)
55         }
56
57         if u.Email == "[email protected]" {
58             t.Log("	Should have an Email.", checkMark)
59         } else {
60             t.Error("	Should have an Email.", ballotX, u.Email)
61         }
62     }
63 }

Listing 9.19 shows a unit test for the /sendjson endpoint. On line 03 you see the name of the package is different from the other tests.

Listing 9.20. handlers/handlers_test.go: lines 01–03
01 // Sample test to show how to test the execution of an
02 // internal endpoint.
03 package handlers_test

This time, as you can see in listing 9.20, the package name also ends with _test. When the package name ends like this, the test code can only access exported identifiers. This is true even if the test code file is in the same folder as the code being tested.

Just like when running the service directly, the routes need to be initialized.

Listing 9.21. handlers/handlers_test.go: lines 17–19
17 func init() {
18     handlers.Routes()
19 }

On line 17 in listing 9.21, an init function is declared to initialize the routes. If the routes aren’t initialized before the unit tests are run, then the tests will fail with an http.StatusNotFound error. Now we can look at the unit test for the /sendjson endpoint.

Listing 9.22. handlers/handlers_test.go: lines 21–34
21 // TestSendJSON testing the sendjson internal endpoint.
22 func TestSendJSON(t *testing.T) {
23     t.Log("Given the need to test the SendJSON endpoint.")
24     {
25         req, err := http.NewRequest("GET", "/sendjson", nil)
26         if err != nil {
27             t.Fatal("	Should be able to create a request.",
28                 ballotX, err)
29         }
30         t.Log("	Should be able to create a request.",
31             checkMark)
32
33         rw := httptest.NewRecorder()
34         http.DefaultServeMux.ServeHTTP(rw, req)

Listing 9.22 shows the declaration of the TestSendJSON test function. The test starts off logging the given need of the test, and then on line 25 it creates an http.Request value. The request value is configured to be a GET call against the /sendjson endpoint. Since this is a GET call, nil is passed as the third parameter for the post data.

Then, on line 33, the httptest.NewRecorder function is called to create an http.ResponseRecorder value. With the http.Request and http.ResponseRecorder values, a call to the ServerHTTP method against the default server multiplexer (mux) is made on line 34. Calling this method mocks a request to our /sendjson endpoint as if it were being made from an external client.

Once the ServeHTTP method call completes, the http.ResponseRecorder value contains the response from our SendJSON function handler. Now we can test the response.

Listing 9.23. handlers/handlers_test.go: lines 36–39
36         if rw.Code != 200 {
37             t.Fatal("	Should receive "200"", ballotX, rw.Code)
38         }
39         t.Log("	Should receive "200"", checkMark)

First, the status of the response is checked on line 36. With any successful endpoint call, a status of 200 is expected. If the status is 200, then the JSON response is decoded into a Go value.

Listing 9.24. handlers/handlers_test.go: lines 41–49
41         u := struct {
42             Name  string
43             Email string
44         }{}
45
46         if err := json.NewDecoder(rw.Body).Decode(&u); err != nil {
47             t.Fatal("	Should decode the response.", ballotX)
48         }
49         t.Log("	Should decode the response.", checkMark)

On line 41 in listing 9.24, an anonymous struct type is declared, and a variable named u is created and initialized to its zero value. On line 46 the json package is used to decode the JSON document from the response into the u variable. If the decode fails, the unit test is ended; otherwise, we validate the values that were decoded.

Listing 9.25. handlers/handlers_test.go: lines 51–63
51         if u.Name == "Bill" {
52           t.Log("	Should have a Name.", checkMark)
53         } else {
54           t.Error("	Should have a Name.", ballotX, u.Name)
55         }
56
57         if u.Email == "[email protected]" {
58             t.Log("	Should have an Email.", checkMark)
59         } else {
60             t.Error("	Should have an Email.", ballotX, u.Email)
61         }
62     }
63 }

Listing 9.25 shows both checks for each value we expect to receive. On line 51 we check that the value of the Name field is "Bill", and then on line 57 the value of the Email field is checked for "[email protected]". If these values match, then the unit test passes; otherwise, the unit test fails. These two checks use the Error method to report failure, so all the fields are checked.

9.2. Examples

Go is very focused on having proper documentation for the code you write. The godoc tool was built to produce documentation directly from your code. In chapter 3 we talked about the use of the godoc tool to produce package documentation. Another feature of the godoc tool is example code. Example code adds another dimension to both testing and documentation.

If you use your browser to navigate to the Go documentation for the json package, you’ll see something like figure 9.8.

Figure 9.8. Listing of examples for the json package

The json package has five examples, and they show in the Go documentation for the package. If you select the first example, you see a view of the example code, as in figure 9.9.

Figure 9.9. A view of the Decoder example in the Go documentation

You can create your own examples and have them show up in the Go documentation for your packages. Let’s look at an example for the SendJSON function from our previous example.

Listing 9.26. handlers_example_test.go
01 // Sample test to show how to write a basic example.
02 package handlers_test
03
04 import (
05     "encoding/json"
06     "fmt"
07     "log"

08     "net/http"
09     "net/http/httptest"
10 )
11
12 // ExampleSendJSON provides a basic example.
13 func ExampleSendJSON() {
14     r, _ := http.NewRequest("GET", "/sendjson", nil)
15     rw := httptest.NewRecorder()
16     http.DefaultServeMux.ServeHTTP(rw, r)
17
18     var u struct {
19         Name  string
20         Email string
21     }
22
23     if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
24         log.Println("ERROR:", err)
25     }
26
27     // Use fmt to write to stdout to check the output.
28     fmt.Println(u)
29     // Output:
30     // {Bill [email protected]}
31 }

Examples are based on existing functions or methods. Instead of starting the function with the word Test, we need to use the word Example. On line 13 in listing 9.26, the name of the example is ExampleSendJSON.

There’s one rule you need to follow with examples. An example is always based on an existing exported function or method. Our example test is for the exported function SendJSON inside the handlers package. If you don’t use the name of an existing function or method, the test won’t show in the Go documentation for the package.

The code you write for an example is to show someone how to use the specific function or method. To determine if the test succeeds or fails, the test will compare the final output of the function with the output listed at the bottom of the example function.

Listing 9.27. handlers_example_test.go: lines 27–31
27     // Use fmt to write to stdout to check the output.
28     fmt.Println(u)
29     // Output:
30     // {Bill [email protected]}
31 }

On line 28 in listing 9.27, the code uses fmt.Println to write the value of u to stdout. The value of u is initialized from making a call to the /sendjson endpoint earlier in the function. On line 29 we have a comment with the word Output:.

The Output: marker is used to document the output you expect to have after the test function is run. The testing framework knows how to compare the final output from stdout against this output comment. If everything matches, the test passes, and you have an example that works inside the Go documentation for the package. If the output doesn’t match, the test fails.

If you start a local godoc server (godoc -http=":3000") and navigate to the handlers package, you can see this all come together, as in figure 9.10.

Figure 9.10. godoc view of the handlers package

You can see in figure 9.10 that the documentation for the handlers package shows the example for the SendJSON function. If you select the SendJSON link, the documentation will show the code, as in figure 9.11.

Figure 9.11. A full view of the example in godoc

Figure 9.11 shows a complete set of documentation for the example, including the code and the expected output. Since this is also a test, you can run the example function with the go test tool, as in figure 9.12.

Figure 9.12. Running the example

After running the test, you see the test passes. This time when the test is run, the specific function ExampleSendJSON is specified with the -run option. The -run option takes any regular expression to filter the test functions to run. It works with both unit tests and example functions. When an example fails, it looks like this figure 9.13.

Figure 9.13. Running an example that fails

When an example fails, go test shows the output that was produced and what was expected.

9.3. Benchmarking

Benchmarking is a way to test the performance of code. It’s useful when you want to test the performance of different solutions to the same problem and see which solution performs better. It can also be useful to identify CPU or memory issues for a particular piece of code that might be critical to the performance of your application. Many developers use benchmarking to test different concurrency patterns or to help configure work pools to make sure they’re configured properly for the best throughput.

Let’s look at a set of benchmark functions that reveal the fastest way to convert an integer value to a string. In the standard library, there are three different ways to convert an integer value to a string.

Listing 9.28. listing28_test.go: lines 01–10
01 // Sample benchmarks to test which function is better for converting
02 // an integer into a string. First using the fmt.Sprintf function,
03 // then the strconv.FormatInt function and then strconv.Itoa.

04 package listing28_test
05
06 import (
07     "fmt"
08     "strconv"
09     "testing"
10 )

Listing 9.28 shows the initial code for the listing28_test.go benchmarks. As with unit test files, the file name must end in _test.go. The testing package must also be imported. Next, let’s look at one of the benchmark functions.

Listing 9.29. listing28_test.go: lines 12–22
12 // BenchmarkSprintf provides performance numbers for the
13 // fmt.Sprintf function.
14 func BenchmarkSprintf(b *testing.B) {
15     number := 10
16
17     b.ResetTimer()
18
19     for i := 0; i < b.N; i++ {
20         fmt.Sprintf("%d", number)
21     }
22 }

On line 14 in listing 9.29, you see the first benchmark, named BenchmarkSprintf. Benchmark functions begin with the word Benchmark and take as their only parameter a pointer of type testing.B. In order for the benchmarking framework to calculate performance, it must run the code over and over again for a period of time. This is where the for loop comes in.

Listing 9.30. listing28_test.go: lines 19–22
19     for i := 0; i < b.N; i++ {
20         fmt.Sprintf("%d", number)
21     }
22 }

The for loop on line 19 in listing 9.30 shows how to use the b.N value. On line 20 we have the call to the Sprintf function from the fmt package. This is the function we’re benchmarking to convert an integer value into a string.

By default, the benchmarking framework will call the benchmark function over and over again for at least one second. Each time the framework calls the benchmark function, it will increase the value of b.N. On the first call, the value of b.N will be 1. It’s important to place all the code to benchmark inside the loop and to use the b.N value. If this isn’t done, the results can’t be trusted.

If we just want to run benchmark functions, we need to use the -bench option.

Listing 9.31. Running the benchmark test
go test -v -run="none" -bench="BenchmarkSprintf"

In our call to go test, we specified the -run option passing the string "none" to make sure no unit tests are run prior to running the specified benchmark function. Both of these options take a regular expression to filter the tests to run. Since there’s no unit test function that has none in its name, none eliminates any unit tests from running. When we issue this command, we get the output shown in figure 9.14.

Figure 9.14. Running a single benchmark

The output starts out specifying that there are no tests to run and then proceeds to run the BenchmarkSprintf benchmark. After the word PASS, you see the result of running the benchmark function. The first number, 5000000, represents the number of times the code inside the loop was executed. In this case, that’s five million times. The next number represents the performance of the code based on the number of nanoseconds per operation, so using the Sprintf function in this context takes 258 nanoseconds on average per call.

The final output from running the benchmark shows ok to represent the benchmark finished properly. Then the name of the code file that was executed is displayed, and finally, the total time the benchmark ran. The default minimum run time for a benchmark is 1 second. You can see how the framework still ran the test for approximately a second and a half. You can use another option called -benchtime if you want to have the test run longer. Let’s run the test again using a bench time of three seconds (see figure 9.15).

Figure 9.15. Running a single benchmark with the -benchtime option

This time the Sprintf function was run twenty million times for a period of 5.275 seconds. The performance of the function didn’t change much. This time the performance was 256 nanoseconds per operation. Sometimes by increasing the bench time, you can get a more accurate reading of performance. For most tests, increasing the bench time over three seconds tends to not provide any difference for an accurate reading. But each benchmark is different.

Let’s look at the other two benchmark functions and then run all three benchmarks together to see what’s the fastest way to convert an integer value to a string.

Listing 9.32. listing28_test.go: lines 24–46
24 // BenchmarkFormat provides performance numbers for the
25 // strconv.FormatInt function.
26 func BenchmarkFormat(b *testing.B) {
27     number := int64(10)
28
29     b.ResetTimer()
30
31     for i := 0; i < b.N; i++ {
32         strconv.FormatInt(number, 10)
33     }
34 }
35
36 // BenchmarkItoa provides performance numbers for the
37 // strconv.Itoa function.
38 func BenchmarkItoa(b *testing.B) {
39     number := 10
40
41     b.ResetTimer()
42
43     for i := 0; i < b.N; i++ {
44         strconv.Itoa(number)
45     }
46 }

Listing 9.32 shows the other two benchmark functions. The BenchmarkFormat function benchmarks the use of the FormatInt function from the strconv package. The BenchmarkItoa function benchmarks the use of the Itoa function from the same strconv package. You can see the same pattern in these two other benchmark functions as in the BenchmarkSprintf function. The call is inside the for loop using b.N to control the number of iterations for each call.

One thing we skipped over was the call to b.ResetTimer, which is used in all three benchmark functions. This method is useful to reset the timer when initialization is required before the code can start executing the loop. To have the most accurate benchmark times you can, use this method.

When we run all the benchmark functions for a minimum of three seconds, we get the result shown in figure 9.16.

Figure 9.16. Running all three benchmarks

The results show that the BenchmarkFormat test function runs the fastest at 45.9 nanoseconds per operation. The BenchmarkItoa comes in a close second at 49.4 nanoseconds per operation. Both of those benchmarks were much faster than using the Sprintf function.

Another great option you can use when running benchmarks is the -benchmem option. It will provide information about the number of allocations and bytes per allocation for a given test. Let’s use that option with the benchmark (see figure 9.17).

Figure 9.17. Running a benchmark with the -benchmem option

This time with the output you see two new values: a value for B/op and one for allocs/op. The allocs/op value represents the number of heap allocations per operation. You can see the Sprintf functions allocate two values on the heap per operation, and the other two functions allocate one value per operation. The B/op value represents the number of bytes per operation. You can see that those two allocations from the Sprintf function result in 16 bytes of memory being allocated per operation. The other two functions only allocated 2 bytes per operation.

There are many different options you can use when running tests and benchmarks. I suggest you explore all those options and leverage this testing framework to the fullest extent when writing your packages and projects. The community expects package authors to provide comprehensive tests when publishing packages for open use by the community.

9.4. Summary

  • Testing is built into the language and Go provides all the tooling you need.
  • The go test tool is used to run tests.
  • Test files always end with the _test.go file name.
  • Table tests are a great way to leverage a single test function to test multiple values.
  • Examples are both tests and documentation for a package.
  • Benchmarks provide a mechanism to reveal the performance of code.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset