Testing
Basic Unit Testing
Unit tests are integrated into the Go programming language and into the tooling. There are third-party packages out there like GoConvey, but I want to avoid using third-party stuff, just because it adds extra dependencies and things to your code.
A unit test is a test of behavior whose success or failure is wholly determined by the correctness of the test and the correctness of the unit under test. — Kevin Henney
So, one thing we’ve to validate and make sure is that our tests are adding value, and also that we’re not over unit testing or under unit testing.
If you want to write tests, then your files have to be in the
format <filename>_test.go
. Otherwise, the testing tool will not find the tests.
These files aren’t going to be built and compiled into your final binaries.
Test files should be in the same package as your code. We might also want to have a folder called test for more than unit test, say integration test.
The package name can be the name only or name_test. If we go with name_test, it allows us to make sure these tests work with the package. The only reason that we don’t want to do this is when we have a function or method that is unexported. However, if we don’t use name_test, it will raise a red flag because if we cannot test the exported API to get the coverage for unexported API then we know are missing something. Thus, 9 out of 10, this is what we want.
My testing philosophies: always make sure that those unexported APIs are very testable. Make sure the exported APIs are very usable. So, it’s a balance.
package example1
import (
"net/http"
"testing"
)
Import Go testing package.
const succeed = "\u2713"
const failed = "\u2717"
These constants, which are the Unicode characters for x and check. They gives us visualization.
// TestDownload validates the http Get function can download content.
func TestDownload(t *testing.T) {
url := "https://www.ardanlabs.com/blog/index.xml"
statusCode := 200
t.Log("Given the need to test downloading content.")
{
t.Logf("\tTest 0:\tWhen checking %q for status code %d", url, statusCode)
{
// Make sure this test code is really as close to how you would
// write production code.
resp, err := http.Get(url)
// Most importantly, if a function fails, like there's an error, you
// need to check your error here. We don't want to see blank
// identifier being used in your test code.
if err != nil {
t.Fatalf("\t%s\tShould be able to make the Get call : %v", failed, err)
}
t.Logf("\t%s\tShould be able to make the Get call.", succeed)
defer resp.Body.Close()
if resp.StatusCode == statusCode {
t.Logf("\t%s\tShould receive a %d status code.", succeed, statusCode)
} else {
t.Errorf("\t%s\tShould receive a %d status code : %d", failed, statusCode, resp.StatusCode)
}
}
}
}
TestBasic
validates the http.Get
function can download content.
Every test will be associated with test function. It starts with the word Test
and the first word after Test
must be capitalized. This function is exported,
or the testing tool will not find it. It uses a testing.T
pointer as its
parameter.
When writing test, we want to focus on usability first. We must write it the same way as we would write it in production. We also want the verbosity of tests. I like the verbosity of tests because when they run in the CI, I’d like to have a lot of information. I’m really about not hiding information.
So we have 3 different methods of t
: Log
or Logf
, Fatal
or Fatalf
,
Error
or Error f
. That is the core APIs for testing.
Log
: Write documentation out into the log output.Error
: Write documentation and also say that this test is failed but we are continue moving forward to execute code in this test.Fatal
: Similarly, document that this test is failed but we are done. We move on to the next test function.
The whole idea of a unit test is to validate that something is working. Normally, we will validate that some API that you’ve written, whether it’s exported or unexported, is behaving, that if I give it this input, it produces this sort of output.
The idea of Given, When, Should:
Given
: Why are we writing this test?When
: What data are we using for this test?Should
: What should happen or not happen?
We are also using the artificial block { }
between a long Log
function.
They help with readability.
go test
command: the testing tool will find that function and the test.
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example1
$ go test
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example1 1.143s
We can also say go test -v
for verbosity, we will get a full output of the
logging.
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example1
$ go test -v
=== RUN TestDownload
--- PASS: TestDownload (2.10s)
example1_test.go:22: Given the need to test downloading content.
example1_test.go:24: Test 0: When checking "https://www.ardanlabs.com/blog/index.xml" for status code 200
example1_test.go:30: ✓ Should be able to make the Get call.
example1_test.go:35: ✓ Should receive a 200 status code.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example1 2.100s
Suppose that we have a lot of test functions, go test -run TestBasic
will
only run the TestBasic
function. The run
flag uses a regular expression to
be able to filter the test functions that you just want to run.
Table Unit Testing
Table tests are a way of writing unit test when you have a base code and you want to throw a lot of different input and expected output around it.
It set up a data structure of input to expected output. This way we don’t need a separate function for each one of these. We just have 1 test function. As we go along, we just add more to the table.
tests := []struct {
url string
statusCode int
}{
{"https://www.ardanlabs.com/blog/index.xml", http.StatusOK},
{"http://rss.cnn.com/rss/cnn_topstorie.rss", http.StatusNotFound},
}
This table is a slice of anonymous struct type. It is the URL we will call and
statusCode
are what we expect.
t.Log("Given the need to test downloading different content.")
{
for i, tt := range tests {
t.Logf("\tTest: %d\tWhen checking %q for status code %d", i, tt.url, tt.statusCode)
{
resp, err := http.Get(tt.url)
if err != nil {
t.Fatalf("\t%s\tShould be able to make the Get call : %v", failed, err)
}
t.Logf("\t%s\tShould be able to make the Get call.", succeed)
defer resp.Body.Close()
if resp.StatusCode == tt.statusCode {
t.Logf("\t%s\tShould receive a %d status code.", succeed, tt.statusCode)
} else {
t.Errorf("\t%s\tShould receive a %d status code : %v", failed, tt.statusCode, resp.StatusCode)
}
}
}
}
Let’s see the output:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example2
$ go test -v
=== RUN TestDownload
--- PASS: TestDownload (7.63s)
example2_test.go:28: Given the need to test downloading different content.
example2_test.go:31: Test: 0 When checking "https://www.ardanlabs.com/blog/index.xml" for status code 200
example2_test.go:37: ✓ Should be able to make the Get call.
example2_test.go:42: ✓ Should receive a 200 status code.
example2_test.go:31: Test: 1 When checking "http://rss.cnn.com/rss/cnn_topstorie.rss" for status code 404
example2_test.go:37: ✓ Should be able to make the Get call.
example2_test.go:42: ✓ Should receive a 404 status code.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example2 7.634s
Mocking Server
The last two tests we created, they were fantastic. But the problem with those 2 tests. They require a connection to the outside world. We’re hitting a live sever. We cannot assume that we always have access to the resources we need.
Therefore, mocking becomes an important part of testing in many cases. I always want to be very careful about mocking, I don’t want to mock databases, I want to use Docker for that. Certain systems, where it’s critical that we know that we’re talking to live systems or binary protocols. But a lot of things can be mocked, and these tests that we did run, could be mocked.
I know that web servers work. So I don’t care about the HTTP protocols, here. What I care about is that if I send this request and I get back the response, that I can process it. I will show you how you can mock a HTTP GET call, already built into Go’s standard library and the language.
import (
"encoding/xml"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
Go gives us this package called httptest. We’re going to use this to mock these GET calls internally.
const succeed = "\u2713"
const failed = "\u2717"
// feed is mocking the XML document we expect to receive.
// Notice that we are using ` instead of " so we can reserve special characters.
var feed = `<?xml version="1.0" encoding="UTF-8"?>
<rss>
<channel>
<title>Going Go Programming</title>
<description>Golang : https://github.com/goinggo</description>
<link>http://www.goinggo.net/</link>
<item>
<pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
<title>Object Oriented Programming Mechanics</title>
<description>Go is an object oriented language.</description>
<link>http://www.goinggo.net/2015/03/object-oriented</link>
</item>
</channel>
</rss>`
// Item defines the fields associated with the item tag in the buoy RSS document.
type Item struct {
XMLName xml.Name `xml:"item"`
Title string `xml:"title"`
Description string `xml:"description"`
Link string `xml:"link"`
}
// Channel defines the fields associated with the channel tag in the buoy RSS
// document.
type Channel struct {
XMLName xml.Name `xml:"channel"`
Title string `xml:"title"`
Description string `xml:"description"`
Link string `xml:"link"`
PubDate string `xml:"pubDate"`
Items []Item `xml:"item"`
}
// Document defines the fields associated with the buoy RSS document.
type Document struct {
XMLName xml.Name `xml:"rss"`
Channel Channel `xml:"channel"`
URI string
}
I’ve created some structs here, and with the XML tag, so we can pull this data when we mock the call and get this raw string back. We can unmarshal it into our real data structures here and do some actual testing.
func mockServer() *httptest.Server {
f := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/xml")
fmt.Fprintln(w, feed)
}
return httptest.NewServer(http.HandlerFunc(f))
}
mockServer
returns a pointer of type httptest.Server
to handle the mock get
call. This mock function calls NewServer
function that will stand up a web
server for us automatically. All we have to give NewServer
is a function of
the Handler
type, which is f
.
f
creates an anonymous function with the signature of ResponseWriter
and
Request
. This is the core signature of everything related to http in Go.
ResponseWriter
is an interface that allows us to write the response out.
Normally when we get this interface value, there is already a concrete type
value stored inside of it that support what we are doing. Request
is a
concrete type that we will get with the request.
This is how it will work.
We will get a mock server started by making NewServer
call. When the request
comes into it, execute f
. Therefore, f
is doing the entire mock.
We will send 200
down the line, set the header to XML and use Fprintln
to
take the ResponseWriter
interface value and feeding with the raw string we
defined above.
// TestDownload validates the http Get function can download content and
// the content can be unmarshaled and clean.
func TestDownload(t *testing.T) {
statusCode := http.StatusOK
// Call the mock sever and defer close to shut it down cleanly.
server := mockServer()
defer server.Close()
// Now, it's just the matter of using server value to know what URL we need
// to use to run this mock. From the http.Get point of view, it is making an
// URL call. It has no idea that it's hitting the mock server. We have
// mocked out a perfect response.
t.Log("Given the need to test downloading content.")
{
t.Logf("\tTest 0:\tWhen checking %q for status code %d", server.URL, statusCode)
{
resp, err := http.Get(server.URL)
if err != nil {
t.Fatalf("\t%s\tShould be able to make the Get call : %v", failed, err)
}
t.Logf("\t%s\tShould be able to make the Get call.", succeed)
defer resp.Body.Close()
if resp.StatusCode != statusCode {
t.Fatalf("\t%s\tShould receive a %d status code : %v", failed, statusCode, resp.StatusCode)
}
t.Logf("\t%s\tShould receive a %d status code.", succeed, statusCode)
// When we get the response back, we are unmarshaling it from XML to
// our struct type and do some extra validation with that as we go.
var d Document
if err := xml.NewDecoder(resp.Body).Decode(&d); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", failed, err)
}
t.Logf("\t%s\tShould be able to unmarshal the response.", succeed)
if len(d.Channel.Items) == 1 {
t.Logf("\t%s\tShould have 1 item in the feed.", succeed)
} else {
t.Errorf("\t%s\tShould have 1 item in the feed : %d", failed, len(d.Channel.Items))
}
}
}
}
Run test:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example3
$ go test -v
=== RUN TestDownload
--- PASS: TestDownload (0.00s)
example3_test.go:82: Given the need to test downloading content.
example3_test.go:84: Test 0: When checking "http://127.0.0.1:33826" for status code 200
example3_test.go:90: ✓ Should be able to make the Get call.
example3_test.go:97: ✓ Should receive a 200 status code.
example3_test.go:103: ✓ Should be able to unmarshal the response.
example3_test.go:106: ✓ Should have 1 item in the feed.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example3 0.004s
I want you to see how fast this test ran. It runs much faster because we know that it didn’t leave my machine. We also see that we have the localhost IP address on a port.
Testing Internal Endpoints
A lot of us are building web APIs using the HTTPS protocols, and we would like to test it as well without manually having to stand up our own server.
The Go standard library HTTP test package supports this.
Below is a basic code to build a non-production level web API.
package main
import (
"log"
"net/http"
// Import handler package that has a set of routes that we will work with.
"github.com/ardanlabs/gotraining/topics/go/testing/tests/example4/handlers"
)
func main() {
handlers.Routes()
log.Println("listener : Started : Listening on: http://localhost:4000")
http.ListenAndServe(":4000", nil)
}
package handlers
import (
"encoding/json"
"net/http"
)
// Routes sets the routes for the web service.
func Routes() {
http.HandleFunc("/sendjson", SendJSON)
}
Routes
has 1 route call sendjson. When that route is executed, it will call
the SendJSON
function.
// SendJSON returns a simple JSON document.
func SendJSON(rw http.ResponseWriter, r *http.Request) {
u := struct {
Name string
Email string
}{
Name: "Bill",
Email: "bill@ardanlabs.com",
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(200)
json.NewEncoder(rw).Encode(&u)
}
SendJSON
has the same signature that we had before using ResponseWriter
and
Request
. That’s how we write handlers in Go. Then we define our literal type,
create an anonymous struct, initialize it and unmarshall it into JSON and send
it down the wire.
Build and run it:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example4
$ go build
$ ./example4
2020/03/19 15:17:01 listener : Started : Listening on: http://localhost:4000
cURL that URL:
$ curl -X POST http://localhost:4000/sendjson
{"Name":"Bill","Email":"bill@ardanlabs.com"}
You’ve just hit that endpoint.
Internal test
Below is how to test the execution of an internal endpoint without having to stand up the server.
package handlers_test
We are using handlers_test
for package name because we want to make sure we
only touch the exported API.
func init() {
handlers.Routes()
}
The init
function is really important. It binds your routes to HTTP handler
function. Not only your app can bind them, but your tests can bind them as well.
If we forget to do this then nothing will work.
// TestSendJSON testing the sendjson internal endpoint.
func TestSendJSON(t *testing.T) {
url := "/sendjson"
statusCode := 200
t.Log("Given the need to test the SendJSON endpoint.")
{
// Create a nil request body GET for the URL.
r := httptest.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(w, r)
t.Logf("\tTest 0:\tWhen checking %q for status code %d", url, statusCode)
{
if w.Code != 200 {
t.Fatalf("\t%s\tShould receive a status code of %d for the response. Received[%d].", failed, statusCode, w.Code)
}
t.Logf("\t%s\tShould receive a status code of %d for the response.", succeed, statusCode)
// If we got the 200, we try to unmarshal and validate it.
var u struct {
Name string
Email string
}
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
t.Fatalf("\t%s\tShould be able to decode the response.", failed)
}
t.Logf("\t%s\tShould be able to decode the response.", succeed)
if u.Name == "Bill" {
t.Logf("\t%s\tShould have \"Bill\" for Name in the response.", succeed)
} else {
t.Errorf("\t%s\tShould have \"Bill\" for Name in the response : %q", failed, u.Name)
}
if u.Email == "bill@ardanlabs.com" {
t.Logf("\t%s\tShould have \"bill@ardanlabs.com\" for Email in the response.", succeed)
} else {
t.Errorf("\t%s\tShould have \"bill@ardanlabs.com\" for Email in the response : %q", failed, u.Email)
}
}
}
}
In order to mock this call, we don’t need the network. What we need to do is create a request and run it through the Mux so we will bypass the network call together, run the request directly through the Mux to test the route and the handler.
w := httptest.NewRecorder()
gives us a pointer to its concrete type called
ResponseRecorder
that already implemented the ResponseWriter
interface.
http.DefaultServeMux.ServeHTTP(w, r)
asks for a ResponseWriter
and a
Request
. This call will perform the Mux and call that handler to test it
without network. When his call comes back, the recorder value w
has the result
of the entire execution. Now we can use that to validate.
Run test:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example4/handlers
$ go test -run TestSendJSON -v
=== RUN TestSendJSON
--- PASS: TestSendJSON (0.00s)
handlers_test.go:32: Given the need to test the SendJSON endpoint.
handlers_test.go:38: Test 0: When checking "/sendjson" for status code 200
handlers_test.go:43: ✓ Should receive a status code of 200 for the response.
handlers_test.go:53: ✓ Should be able to decode the response.
handlers_test.go:56: ✓ Should have "Bill" for Name in the response.
handlers_test.go:62: ✓ Should have "bill@ardanlabs.com" for Email in the response.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example4/handlers 0.003s
Example test
This is another type of test in Go. Examples are both documentations and tests.
package handlers_test
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
)
// ExampleSendJSON provides a basic example example.
func ExampleSendJSON() {
r := httptest.NewRequest("GET", "/sendjson", nil)
w := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(w, r)
var u struct {
Name string
Email string
}
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
log.Println("ERROR:", err)
}
fmt.Println(u)
// Output:
// {Bill bill@ardanlabs.com}
}
Naming convention ExampleSendJSON
: Notice that we are binding that Example
to our SendJSON
function.
Godoc
If we execute godoc -http :3000
, Go will generate for us a server that present
the documentation of our code. The interface will looks familiar like the
official golang.org interface, but then inside “Packages” section are our local
packages.
Example is also a test
Example functions are a little bit more concrete in term of showing people how to use our API. More interestingly, Examples are not only for documentation but they can also be tests. For them to be tests, we need to add a comment at the end of the functions: one is Output and one is expected output. If we change the expected output to be something wrong then, the complier will tell us when we run the test. Example:
fmt.Println(u)
// Output:
// {Bill bill@ardanlabs.com}
So anything in this example writes to standard out, we can now validate.
So, let’s go and test that.
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example4/handlers
$ go test -run ExampleSendJSON
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example4/handlers 0.003s
Example tests are powerful. They give users examples how to use the API and validate that the APIs and examples are working.
Sub Tests
Sub test let us streamline our test functions, filters out command-line level large tests into smaller sub tests.
Sub tests help when we do table tests because one of the things that we have with table tests is the ability to do this data-driven testing. Sub tests let us filter a piece of data at the command line-level.
// TestDownload validates the http Get function can download content and
// handles different status conditions properly.
func TestDownload(t *testing.T) {
// Data for our standard table tests.
// name field will give us the ability to name each piece of data and then
// filter things on the command-line.
tests := []struct {
name string
url string
statusCode int
}{
{"statusok", "https://www.ardanlabs.com/blog/index.xml", http.StatusOK},
{"statusnotfound", "http://rss.cnn.com/rss/cnn_topstorie.rss", http.StatusNotFound},
}
t.Log("Given the need to test downloading different content.")
{
// Range over our table but this time, create an anonymous function (tf)
// that takes a testing T parameter. This is a test function inside a test
// function. What nice about it is that we will have a new function for
// each set of data that we have in our table. Therefore, we will end up
// with 2 different functions here.
for i, tt := range tests {
tf := func(t *testing.T) {
t.Logf("\tTest: %d\tWhen checking %q for status code %d", i, tt.url, tt.statusCode)
{
resp, err := http.Get(tt.url)
if err != nil {
t.Fatalf("\t%s\tShould be able to make the Get call : %v", failed, err)
}
t.Logf("\t%s\tShould be able to make the Get call.", succeed)
defer resp.Body.Close()
if resp.StatusCode == tt.statusCode {
t.Logf("\t%s\tShould receive a %d status code.", succeed, tt.statusCode)
} else {
t.Errorf("\t%s\tShould receive a %d status code : %v", failed, tt.statusCode, resp.StatusCode)
}
}
}
t.Run(tt.name, tf)
}
}
}
Run this test:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example5
$ go test -run TestDownload -v
=== RUN TestDownload
=== RUN TestDownload/statusok
=== RUN TestDownload/statusnotfound
--- PASS: TestDownload (3.62s)
example5_test.go:34: Given the need to test downloading different content.
--- PASS: TestDownload/statusok (3.06s)
example5_test.go:38: Test: 0 When checking "https://www.ardanlabs.com/blog/index.xml" for status code 200
example5_test.go:44: ✓ Should be able to make the Get call.
example5_test.go:49: ✓ Should receive a 200 status code.
--- PASS: TestDownload/statusnotfound (0.56s)
example5_test.go:38: Test: 1 When checking "http://rss.cnn.com/rss/cnn_topstorie.rss" for status code 404
example5_test.go:44: ✓ Should be able to make the Get call.
example5_test.go:49: ✓ Should receive a 404 status code.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example5 3.622s
It executed it in series, one after the other.
Now, let’s say I just wanted to run “statusok”. I didn’t have to go into my code and comment out any particular line in my table.
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example5
$ go test -run TestDownload/statusok -v
=== RUN TestDownload
=== RUN TestDownload/statusok
--- PASS: TestDownload (1.37s)
example5_test.go:34: Given the need to test downloading different content.
--- PASS: TestDownload/statusok (1.37s)
example5_test.go:38: Test: 0 When checking "https://www.ardanlabs.com/blog/index.xml" for status code 200
example5_test.go:44: ✓ Should be able to make the Get call.
example5_test.go:49: ✓ Should receive a 200 status code.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example5 1.372s
We can run all of this data in parallel when it’s reasonable and practical to do so.
// TestParallelize validates the http Get function can download content and
// handles different status conditions properly but runs the tests in parallel.
func TestParallelize(t *testing.T) {
tests := []struct {
name string
url string
statusCode int
}{
{"statusok", "https://www.goinggo.net/post/index.xml", http.StatusOK},
{"statusnotfound", "http://rss.cnn.com/rss/cnn_topstorie.rss", http.StatusNotFound},
}
t.Log("Given the need to test downloading different content.")
{
for i, tt := range tests {
tf := func(t *testing.T) {
t.Parallel()
t.Logf("\tTest: %d\tWhen checking %q for status code %d", i, tt.url, tt.statusCode)
{
resp, err := http.Get(tt.url)
if err != nil {
t.Fatalf("\t%s\tShould be able to make the Get call : %v", failed, err)
}
t.Logf("\t%s\tShould be able to make the Get call.", succeed)
defer resp.Body.Close()
if resp.StatusCode == tt.statusCode {
t.Logf("\t%s\tShould receive a %d status code.", succeed, tt.statusCode)
} else {
t.Errorf("\t%s\tShould receive a %d status code : %v", failed, tt.statusCode, resp.StatusCode)
}
}
}
t.Run(tt.name, tf)
}
}
}
The only difference here is that we call t.Parallel()
function inside each of
these individual sub test function. This tells the testing tool to run each as a
separate or independent Goroutine.
See what happens when we run this:
~/m/dev/work/repo/experiments/go/ultimate-go/testing/tests/example5
$ go test -run TestParallelize -v
=== RUN TestParallelize
=== RUN TestParallelize/statusok
=== PAUSE TestParallelize/statusok
=== RUN TestParallelize/statusnotfound
=== PAUSE TestParallelize/statusnotfound
=== CONT TestParallelize/statusok
=== CONT TestParallelize/statusnotfound
--- PASS: TestParallelize (0.00s)
example5_test.go:73: Given the need to test downloading different content.
--- PASS: TestParallelize/statusok (0.48s)
example5_test.go:79: Test: 1 When checking "http://rss.cnn.com/rss/cnn_topstorie.rss" for status code 404
example5_test.go:85: ✓ Should be able to make the Get call.
example5_test.go:90: ✓ Should receive a 404 status code.
--- PASS: TestParallelize/statusnotfound (0.48s)
example5_test.go:79: Test: 1 When checking "http://rss.cnn.com/rss/cnn_topstorie.rss" for status code 404
example5_test.go:85: ✓ Should be able to make the Get call.
example5_test.go:90: ✓ Should receive a 404 status code.
PASS
ok github.com/cedrickchee/ultimate-go/testing/tests/example5 0.481s
Look how fast it ran. It wasn’t a second anymore.
Sub tests are fantastic when we’ve got these data-driven tests, because it allow us to not just be able to isolate on the command line one given piece of data over the other, but run all of this stuff in parallel, which means our unit tests can run faster.
Code Coverage
In one of the earlier sections, I told you, “how do you know when you’re done with a piece of code?” One of the things we said was code coverage.
Basic code coverage commands
Look at the coverage of all test cases:
$ go test -cover
PASS
coverage: 100.0% of statements
ok github.com/cedrickchee/ultimate-go/testing/tests/example4/handlers 0.003s
Get the cover profile and write it out to c.out
:
$ go test -coverprofile c.out
I’ve generated a c.out
file with the profiling information for the cover
report. But how do I look at this coverage? Well, go tool cover. Get an HTML
representation in the browser:
$ go tool cover -html=c.out
Making sure your tests cover as much of your code as possible is critical. Make sure you’re at that 70 to 80% code coverage. Go’s testing tool allows you to create a profile for the code that is executed during all the tests and see a visual of what is and is not covered.