By William Kennedy with Brian Ketelsen and Erik St. Martin
Excerpted from the book Go in Action.
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 if 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 does not produce an error. This could be a test that validates the code can insert a job record into the database successfully.
Other unit tests may test negative path scenarios to make sure the code not only produces 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 the error is reported and the correct error context is provided. In the end, the code we write must be predictable no matter how it is called or executed.
There are several ways in Go to write unit tests. There are basic tests, which test a specific piece of code for a single set of parameters and result. There are table tests, which also tests 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 our own web services, there are way to test calls coming into to the service without ever needing to run the service itself.specific piece of code for a single set of parameters and result.
For the purposes of this article, we’ll work through an example of a basic unit test.
Listing 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("\tWhen checking \"%s\" for status code \"%d\"", 20 url, statusCode) 21 { 22 resp, err := http.Get(url) 23 if err != nil { 24 t.Fatal("\t\tShould be able to make the Get call.", 25 ballotX, err) 26 } 27 t.Log("\t\tShould be able to make the Get call.", 28 checkMark) 29 30 defer resp.Body.Close() 31 32 if resp.StatusCode == statusCode { 33 t.Logf("\t\tShould receive a \"%d\" status. %v", 34 statusCode, checkMark) 35 } else { 36 t.Errorf("\t\tShould receive a \"%d\" status. %v %v", 37 statusCode, ballotX, resp.StatusCode) 38 } 39 } 40 } 41 }
Listing 1 shows a unit test that is testing the Get function from the http package. It is 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 following test results:
Figure 1: Output from the basic unit test.
There are a lot of little things 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 1, you will see 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 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 2, we can see the importing of the testing package on line 06. The testing package provides us with 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 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 called TestDownload and can be seen on line 13 in listing 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 return no value. If we don’t follow these conventions, the testing framework will not recognize the function as a test function and none of the tooling will work against it.
The pointer of type testing.T is very important. It provides the mechanism for reporting the output and status of each test. There is 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 is 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 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 {
We see on lines 14 and 15 in listing 4 two variables that are declared and initialized. These variables contain the URL we want to test and the status we expect back from the response. Then on line 17, the t.Log method is used to write a message to the test output. There is also a format version of this method called t.Logf. If the verbose option (-v) is not used when calling go test, we will not 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 5: listing01_test.go : lines 19 – 21
19 t.Logf("\tWhen checking \"%s\" for status code \"%d\"", 20 url, statusCode) 21 {
We 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 6: listing01_test.go : lines 22 – 30
22 resp, err := http.Get(url) 23 if err != nil { 24 t.Fatal("\t\tShould be able to make the Get call.", 25 ballotX, err) 26 } 27 t.Log("\t\tShould be able to make the Get call.", 28 checkMark) 29 ' 30 defer resp Body.Close()
The code in listing 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 is used to let 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 stop the execution of this particular test function. If there are other test functions that have not run yet, they will be executed. There is also a formatted version of this method 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 7: listing01_test.go : lines 32 – 41
One line 32 in listing 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 else we use the t.Errorf method. Since the t.Errorf method does not stop the execution of the test function, if there were more test to conduct after line 38, the unit test would continue to be executed.
If the t.Fatal t.Error or functions are not called by a test function, the test will be considered as passing.
If we look at the output of the test one more time, we can see how it all comes together:
Figure 2: Output from the basic unit test.
In figure 2 we 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.
Go in Action: Basic Unit Test By William Kennedy with Brian Ketelsen and Erik St. Martin Excerpted from the book Go in Action. |