Go Unit Testing with examples

Unit testing is a key aspect of software development that helps ensure the correctness and reliability of the code. In Go, the process of unit testing is relatively straightforward, but it requires a little bit of planning and effort.

In this article, we’ll cover the basics of unit testing in Go, including how to write unit tests, run them, and interpret the results. We’ll also provide some practical examples of how to apply unit testing to real-world scenarios.

Writing Unit Tests in Go

The first step in writing a unit test is to create a new file with the suffix _test.go in the same package as the code being tested. For example, if you’re testing a package called math, you would create a file named math_test.go in the same directory as the math package.

Once you’ve created the test file, you need to import the testing package and define a function with the signature func TestXxx(t *testing.T). The function name should start with Test and be followed by a descriptive name that indicates what the test is testing. For example, if you’re testing a function named Add, you might define a test function called TestAdd.

Here’s an example of a simple unit testing:

1
2
3
4
5
6
7
8
9
10
11
package math

import "testing"

func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d, expected 5", result)
}
}

In this example, we’re testing the Add function, which takes two integers and returns their sum. We call Add with the arguments 2 and 3, and then compare the result to the expected value of 5. If the result is not equal to 5, we use the t.Errorf function to report an error.

Running Unit Tests in Go

To run all the unit tests in a package, you can use the go test command followed by the package name. For example, to run all the tests in the math package, you would enter the following command:

1
$ go test math

This will run all the unit tests defined in the math package and display the results. By default,go test prints a summary of the test results, indicating how many tests passed and how many failed. You can also use the -v flag to get more detailed output, including the name of each test function and its result.

If you want to run a specific test function, you can use the -run flag followed by a regular expression that matches the name of the test function. For example, to run the TestAdd function in the math package, you would enter the following command:

1
$ go test -run TestAdd math

This will run only the TestAdd function in the math package and display the results.

Analyzing Test Coverage in Go

Test coverage is a metric that measures how much of the code is exercised by the unit tests. In Go, you can use the -cover flag with the go test command to generate a coverage report.

To generate a coverage report for the math package, you would enter the following command:

1
$ go test -cover math

This will run all the unit tests in the math package and display the results, including the percentage of code covered by the tests.

Here’s an example of a coverage report:

1
ok math 0.001s coverage: 100.0%

Let’s dive deeper into writing more complex unit tests with practical examples.

Practical Examples

Testing an HTTP handler

In this example, we’ll create a simple HTTP server that serves a single endpoint and write a unit test for the endpoint handler. We’ll use the built-in net/http/httptest package to create a test server and send HTTP requests to it.

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
package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}

expected := ""
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}

In this example, we create a new HTTP request for the root endpoint ("/") and use the httptest.NewRecorder function to create a new ResponseRecorder to capture the response from the server.

We then define a simple HTTP handler function that always returns a status code of 200. We call this handler function with the ServeHTTP method of the http.Handler interface, passing in the ResponseRecorder and Request objects.

Finally, we use the Code and Body methods of the ResponseRecorder object to check the status code and body of the response, and report any errors using the t.Errorf function.

Testing a database function

In this example, we’ll create a function that interacts with a database and write a unit test for it. We’ll use the built-in testing/quick package to generate random input values for the function.

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
38
39
40
41
42
package main

import (
"database/sql"
"fmt"
"testing"
"testing/quick"
)

func GetUserName(db *sql.DB, id int) (string, error) {
var name string
err := db.QueryRow("SELECT name FROM users WHERE id=?", id).Scan(&name)
if err != nil {
return "", err
}
return name, nil
}

func TestGetUserName(t *testing.T) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
t.Fatal(err)
}
defer db.Close()

f := func(id int) bool {
name, err := GetUserName(db, id)
if err != nil {
t.Error(err)
return false
}
if name == "" {
t.Errorf("empty name for id=%d", id)
return false
}
return true
}

if err := quick.Check(f, nil); err != nil {
t.Error(err)
}
}

In this example, we define a function called GetUserName that takes a database connection and an ID, and returns the name of the user with that ID. We use the QueryRow method of the sql.DB object to execute a SQL query and retrieve the name from the result.

We also define a unit test function called TestGetUserName that uses the testing/quick package to generate random input values for the GetUserName function. We pass in a function f that takes an ID and returns a boolean


In conclusion, unit testing in Golang is a critical component of writing robust and reliable software applications. It helps developers ensure that their code is functioning correctly, identify bugs and errors before they cause issues in production, and maintain code quality and stability over time.

In the practical examples discussed, we showed how to test an HTTP handler and a database function, demonstrating how unit testing can be used to validate different aspects of a software system.

Overall, Golang’s built-in testing framework provides developers with the tools they need to create comprehensive and effective unit tests quickly and efficiently. By following best practices for unit testing and writing tests that cover all aspects of their code, developers can be confident that their software is of high quality and meets their users’ needs.