Unit Tests - Practical Go Lessons-19
Unit Tests - Practical Go Lessons-19
com/chap-19-unit-tests
• Test case
• Test function
• Assertion
• Code coverage
2 Introduction
Here is a function :
This function computes the price of a booking. It seems right, no? How to be sure that the amount returned is correct? We can run it with
some data as argument and check its result :
1 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
package main
import "fmt"
func main() {
price := totalPrice(3, 10000, 132)
fmt.Println(price)
}
The program outputs 30132 which is equivalent to 301.32$ . We should divide it by 100 to get an amount in dollars.
Is it correct? Let’s compute it by hand. The total price of a room is the number of nights multiplied by the rate plus the city tax : 3 × (100 +
1, 32) = 3 × 101.32 = 303.96. Have you spotted the bug in the function?
This statement :
This way, the function returns the right answer. What if our program checks it directly?
// unit-test/intro/main.go
package main
import "fmt"
//...
func main() {
price := totalPrice(3, 10000, 132)
if price == 30396 {
fmt.Println("function works")
} else {
fmt.Println("function is buggy")
}
}
The program itself will check if the function implementation is correct. No surprises it outputs the function works ! This program is a unit
test!
We take individual parts of the system to test them. In other words, we check that individual parts of the system work. The whole system is
not tested.
The unit test is created and run by the developer of the code. With this tool, we can check that our methods and functions run as expected.
Unit tests are focused exclusively on checking that those small programming units are working.
Some developers will argue that unit tests are useless. Often they say that when they develop their code, they are testing permanently that
the system work. For instance, a web developer that has to create a website will often have two screens, the one with the source code and the
one with the program running. When he wants to implement something new, he will start with the code and checks if it works.
This process is purely manual and depends on the experience of the developer on the system. A newly hired might not detect errors and
breaking changes. What if we can run those tests automatically? Imagine that you can run those tests each time you build your Go program!
Or even better, you could run them each time you make a change to a package!
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
2 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
To better understand what a test case is, let’s take an example. Imagine that you have developed a function to capitalize a string. We will
build a test case to check it.
• A way to assert that the actual returned value of our function is the one that is expected. We could use string comparison features of
Go to check that the two strings are equal. We can also use a Go package to do it. This part of the unit test is called the assertion
• Unit tests will control that functions and methods work as expected. Without unit tests, developers test their functionality during the
development phase. Those tests are not reproducible. After the development of the feature, those manual tests are no longer run.
• If they write their tests into the project sources, they can run those tests later. They protect the project against nasty regressions
(development of new features breaks something in the code of another one).
• The presence of unit test can be a customer requirement. It seems to be pretty rare, but some specifications include test coverage
requirements.
• A better focus on API design is also generally observed when developers write unit tests. You have to call the function you are
developing; as a consequence you can see improvements. This focus is even bigger if you use the TDD method.
• Unit tests also serve as code documentation. Users that want to know how to call a specific function can take a look at the unit test to
get their answer immediately.
3 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
You can see that there is a naming pattern: there is a file named xxx_test.go for each file named xxx.go.
When you build your program, the file named xxx_test.go will be ignored by the compiler.
// unit-test/basic/foo/foo.go
package foo
import "fmt"
// unit-test/basic/foo/foo_test.go
package foo
import "testing"
You can see that this source file is part of the foo package. We have imported the package testing from the standard library (that we will use
later).
4 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
A single function is defined: TestFoo. This function takes as input a pointer to testing.T (*testing.T).
• The first part of the test function name is the word Test. It is fixed. It’s always "Test"
• The second part is often the name of the function you want to test. It must start with a capital letter.
// unit-test/basic/foo/foo_test.go
package foo
import "testing"
We first define a variable expected that holds our expected result. Then we define the variable actual that will hold the actual return value
of the function Foo from the package foo .
Please remember those two terms : actual and expected. They are classic variables names in the context of testing.
• The actual variable holds the execution result of the unit of the code we want to test.
Then the test continues with an assertion. We test the equality between the actual value and the expected one. If that’s not the case, we
make the test fail by using a t.Errorf method (that is defined on the type struct T from the package testing ) :
When the test function returns without calling a failure method, then it is interpreted as a success.
• Error : will log and marks the test function as failed. Execution will continue.
• Errorf : will log (with the specified format) and marks the test function as failed. Execution will continue.
• FailNow : this one will mark the test as failed and stop the execution of the current test function (if you have other assertions, they will
not be tested).
You also have the methods Fatal and Fatalf that will log and call internally FailNow .
5 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
6 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
7 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
The testdata folder inside a package can hold files used in tests (tree view of standard library archive/zip)
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
8 Assertion libraries
The Go standard library gives all the necessary tools to build your unit test without external libraries. Despite this fact, it is common to see
projects that use external “assertion libraries”. An assertion library exposes many functions and methods to build assertions. One very popular
module is github.com/stretchr/testify.
$ go get github.com/stretchr/testify
As an example, this is the previous unit test written with the help of the package assert from the module github.com/stretchr/testify :
// unit-test/assert-lib/foo/foo_test.go
package foo
import (
"testing"
"github.com/stretchr/testify/assert"
)
Other libraries exist a quick search on GitHub can give you some additional references: https://github.com/search?l=Go&
q=assertion+library&type=Repositories
$ cd go/src/gitlab.com/loir402/foo
$ go test
PASS
ok gitlab.com/loir402/foo 0.005s
This command will run all the unit tests for the package located into the current directory. For instance, if you want to run the unit tests of the
path package of the current directory :
$ cd /usr/local/go/src/path
$ go test
$ go test ./...
8 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
$ go test
--- FAIL: TestFoo (0.00s)
foo_test.go:9: Expected Bar do not match actual Foo
FAIL
exit status 1
FAIL gitlab.com/loir402/foo 0.005s
You can note that the test result is more verbose in the case of a failure. It will indicate which test case fail by printing the test case name
( TestFoo ). it will also give you the line of the test that fails ( foo_test.go:9 ).
Then you can see that the system is printing the error message that we have told him to print in the case of a failure.
The program exits with a status code of 1, allowing you to autodetect it if you want to create continuous integration tools.
The go vet command is part of the Go toolchain. It performs syntax verification on your source code to detect potential errors.
This command has a whole list of checks; when you run a go test, only a small subset is launched :
atomic
will detect the bad usages of the package sync/atomic
bool
this check will verify the usages of boolean conditions.
buildtags
when you run a go test you can specify build tags to the command line, this check will verify that you have correctly formed build tags in the
command you type.
nilfunc
checks that you never compare a function with nil
Running a set of go vet command automatically before launching unit tests is a brilliant idea. It can make you discover mistakes before
they cause harm to your program!
$ go test -c
9 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
// unit-test/table-test/price/price_test.go
package price
import "testing"
// test case 3
expected = uint(224)
actual = totalPrice(2, 100, 12)
if expected != actual {
t.Errorf("Expected %d does not match actual %d", expected, actual)
}
• We have 3 test cases; each test case follow the previous one
• This is a good approach; it works as expected. However, we can use the table test approach that can be more convenient :
10 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
// unit-test/table-test/price/price_test.go
package price
import "testing"
• We create a type struct named parameters . Each field of that struct is a function parameter of the function under test
• A slice named tests containing elements of type testCase is created. This is here that we will manually define each test case
◦ Parameters :
▪ An anonymous function that contains the test to run (its signature is similar to a standard test case)
• At each iteration, we compare what we got (the actual value) to what we expect.
11 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
• The run result gives the information that three subtests are run.
• It also gives the result for each test along with the name of the test.
$ go test
Here nothing is added, just go test . In this mode, Go will build the package located in the current directory.
All unit tests of the project will not be executed, but only the ones defined at the current package level. Some IDEs will run this command
each time you save a source file; that’s a pretty good idea because each time you modify a package file, you can check that the unit tests are
passing.
For instance, if you have a project that defines a pkgName strings you can run the following command :
go test modulePath/pkgName
This command will work in any project directory. It will run the test of the package pkgName from the module modulePath .
11.3 Caching
When you are in package list mode go will cache the test result of successful tests. This mechanism has been developed to avoid testing
packages multiple time.
$ go test strings
ok strings 4.256s
You can see here that the unit test’s time is 4.256s, which is quite long.
$ go test strings
ok strings (cached)
You can see here that the result is instantaneous, and instead of the duration, the (cached) is displayed. It means that go has retrieved the
cached version of the test result.
12 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
Let’s take an example, imagine that you are using the environment variable MYENV inside your test script :
The first time, when you execute the test with the environment variable set to "BAR" , then the test will run :
At the second run of the same command, Go will retrieve the test result directly from cache :
ok gitlab.com/loir402/foo (cached)
But if you change the value of the environment variable MYENV then the test will be executed :
Here we open the file testdata/lol.txt. If we run the test for the first time, it is executed, and it’s cached.
If we modify the content of testdata/lol.txt and rerun the test, it will be executed because the file’s content has changed, then the test
conditions are not the same.
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
You need to add a call to the Parallel method from the package testing to allow your test to be run concurrently by the go command line.
13 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
Here we have 3 unit tests, that are testing nothing but that just wait for 300 milliseconds each. We did not add any assertion on purpose to
facilitate the reading of the source code.
$ go test
PASS
ok gitlab.com/loir402/corge 0.913s
t.Parallel()
at the beginning of the test. This simple method call will increase the running speed of our test :
$ go test
PASS
ok gitlab.com/loir402/corge 0.308s
We have divided the running time by 3 ! This gain of time is precious for the development team, so use this feature when you build your unit
tests !
14 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
Here we retrieve the second command-line argument. Note that os.Args is a slice of strings ( []string ), and the first index (0) is occupied
by internal values of the go test command line (the emplacement of the cached build).
To pass arguments when the test run, we can use the flag -args :
You can add as many flags as you want with this method. Note that -args is not part of the cacheable flags.
13.2 Flags
You can pass all the existing build flags to the go test command line. In addition to that, specific testing flags are available.
We intentionally do not cover benchmark-specific flags. We will explain them in a dedicated chapter.
$ go test -cover
PASS
coverage: 100.0% of statements
ok gitlab.com/loir402/foo 0.005s
This flag allows you to choose a method to compute the coverage percentage. (the default is “set”, other values available are “count” or
“atomic”). For more information about the computation method, see the specific section.
You can specify that coverage data will be computed only for a subset of your project’s package.
This flag will define the maximum number of tests that will be run in parallel. By default, it is set to the variable GOMAXPROCS
By default, the timeout is set to 10 minutes when you run your tests. Consequently, unit tests that run for more than 10 minutes will panic. If
your test suite needs more than 10 minutes to be executed, you can overwrite that setting and set a specific duration (the value of this flag is
a string that will be parsed as a time.Duration)
Verbose mode will display the names of the test functions as they are run :
The log is pretty long for three tests because we run them in parallel. Tests that are not running in parallel do not log the PAUSE and CONT
steps :
15 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
When you run your test go will automatically run go vet for a set of common errors (see [subsec:Go-testvet]). If you want to deactivate it
totally (I do not recommend), you can set this flag to off. But you can also add complimentary checks.
14 Code Coverage
• Is a project sufficiently tested?
Code coverage answers those questions. Code coverage is a measure of how a project is tested. It is often given as a percentage.
The measure’s definition is not unique, and different code coverage definitions exist.
The go tool can compute this code coverage statistic for you. Three modes (or computation methods) are available.
To output the test coverage of your code, run the following command :
$ go test -cover
PASS
coverage: 66.7% of statements
ok go_book/testCoverage 0.005s
You see that a new line has been added to the test summary. It gives the percentage of code coverage.
We will go through this figure’s different computation methods in the next sections.
Perfect test coverage is 100%, meaning that all the code statements have been tested.
package testCoverage
This package defines one single function. Inside this function, a conditional statement discriminates two cases. Input numbers under ten and
numbers above.
16 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
go test -cover
PASS
coverage: 66.7% of statements
ok go_book/testCoverage 0.005s
To generate this file, you have to use two commands in your terminal :
mode: set
unit-test/coverage/testCoverage.go:3.29,4.17 1 1
unit-test/coverage/testCoverage.go:4.17,6.3 1 1
unit-test/coverage/testCoverage.go:6.8,8.3 1 0
This file details the blocks of code of your application. Each line represent a “block”. At the end of each line you can see two figures: the total
number of statements and the number of statements covered (see figure 2)
17 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
Coverprofile file[fig:Coverprofile-file]
This file is not easily readable. From this file, you can generate a nice HTML file like in the figure 1. To do this, type the following command :
It will create an HTML page, store it (not in your project directory) and open it on a browser.
We can increase that percentage to 100% by integrating a test of the remaining statement (the else part of our condition) :
This will lead to a coverage of 100%. All the statements of our code are covered.
All statements are covered, but the first one (the conditional statement if) is tested twice. During the execution of the second test, the test
number < 10 is evaluated.
The coverprofile in count mode has not the same layout as you can see in figure 3.
18 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
The coverprofile has the same layout, but the last figure represents the number of time the statement is tested :
mode: count
unit-test/coverage/testCoverage.go:3.29,4.17 1 2
unit-test/coverage/testCoverage.go:4.17,6.3 1 1
unit-test/coverage/testCoverage.go:6.8,8.3 1 1
On the second line of this profile, you can see that the first code block (starts at 3.29 and ends at 4.17) has 1 statement tested two times.
To demonstrate it, I have modified the BazBaz function to make it even more silly by adding useless goroutines :
// unit-test/coverage/testCoverage.go
package testCoverage
import (
"fmt"
"sync"
)
We will launch 100 useless concurrent tasks that just make an assignment: set useless to the number + 2. We use waitgroups to ensure that
all our concurrent tasks will execute before the program ends. We do not modify the unit tests.
19 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
If we use the count mode, the result is not accurate. For the last block of code (from 18.60 to 22.2), the count mode has found that we have
tested the statement 197 times. The atomic mode has found that we have tested it 200 times which is the correct value.
Note that this cover mode will add overhead to the coverprofile creation.
Historically this method has emerged with the development of the XP methodology in the late nineties. This method has been widespread
among the community, and authors like Robert C. Martin1 have contributed to its adoption.
Let’s jump right away to an example in Go. Our objective is to build a function that will count the number of vowels in a string. We first begin
by creating a test case (that will fail because we have not created the function :
// unit-test/tdd/tdd_test.go
package tdd
import "testing"
Here we are calling the function VowelCount with the sentence "I love you" . In this sentence, we have five vowels; our expected result is
the integer 4. As usual, we compare the actual number and the expected.
$ go test
# go_book/tdd [go_book/tdd.test]
./tdd_test.go:7:12: undefined: VowelCount
FAIL go_book/tdd [build failed]
Now we can implement our function. We start by creating a map of vowels from the alphabet.
// unit-test/tdd/tdd.go
package tdd
Then we create the function. It will take each letter of the sentence iteratively and check if the vowel is on the map :
20 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
// unit-test/tdd/tdd.go
package tdd
//...
The char is the Unicode code point. That’s why we have to convert it to a string to make our script work. Let’s run the test again to see if our
implementation works :
$ go test
--- FAIL: TestVowelCount (0.00s)
tdd_test.go:9: actual 4, expected 5
FAIL
exit status 1
It seems not to work. What could be wrong with our code? One letter seems to be skipped. There is something we missed, here our test
string comes with the I capitalized, but we compare it to lowercase letters. That’s a bug. We want to count also capitalized vowels.
2. Each letter should be converted to lowercase and then compared with the existing map.
• In the first option, we have to spend precious time converting each letter,
• Whereas in the second solution, we are just performing a map lookup which is really fast (O(1)2).
$ go test
PASS
ok go_book/tdd 0.005s
15.0.0.1 Advantages
• The developer is forced to create a function that can be tested.
• When we wrote our test, we have chosen our function’s signature. This is an act of design focused on the use of our functionality. In a
certain measure, we are designing our API in an user-centric approach. We are using our API before actually implementing it forces us
to keep things simple and usable.
• With this method every single function of our source code is tested.
You might argue that this way of developing is not very natural. This feeling is normal. Many developers (especially young ones) that I have
met are reluctant to do the tests and prefer to develop the functionality in a minimal amount of time.
21 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
• Using TDD has generated a reduction of 50% of defect density for a company composed of 9 developers. With a minimal impact on
productivity.[@maximilien2003assessing]
• For another company, the reduction percentage was 40%, with no impact at all on the productivity of the team (9 developers)
[@williams2003test].
• The use of unit test leads to a better information flow in the company [@kaufmann2003implications].
• Another study that was conducted in a class of undergraduate class of computer science has demonstrated a reduction of 45% of defects
per thousand lines of code [@edwards2003using].
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
16 Test yourself
16.1 Questions
1. In a Go program where test files are stored?
2. You develop a function named ShippingCost . What is the conventional signature of its test function?
3. In a test, you need to load data from a separate file; where can you store it?
16.2 Answers
1. In a Go program where test files are stored?
1. Test files are stored in the same directory as the package source files.
2. You develop a function named ShippingCost . What is the conventional signature of its test function?
1. Note the string "Test" at the beginning of the function, this is mandatory
2. The characters after “Test” are free, but they must begin with a capitalized letter or an underscore (_)
3. In a test, you need to load data from a separate file; where can you store it?
1. You can create a directory named “testdata” in the directory containing the source files of the package.
2. You can put in this folder files loaded in your test cases.
1. In the context of unit tests, an assertion is a boolean expression (i.e., An expression that will be evaluated to true or false).
2. A “traditional” example is :
actual == expected
17 Key Takeaways
• Writing unit tests is a good practice :
22 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
• The name of a test function should begin with Test , the next characters is either an underscore or a capitalized letter
• The name of the function under test is generally contained in the test function name.
• Table tests are a convenient way to test several test cases in a single function.
• Test-Driven Development is a method that can increase the quality of your code and reduce defects.
◦ The unit test fails first; the aim is then to make it pass.
Bibliography
• [institute1990ieee] Electrical, Institute of, and Electronics Engineers. 1990. “IEEE Standard Glossary of Software Engineering Terminology:
Approved September 28, 1990, IEEE Standards Board.” In. Inst. of Electrical; Electronics Engineers.
• [runeson2006survey] Runeson, Per. 2006. “A Survey of Unit Testing Practices.” IEEE Software 23 (4): 22–29.
• [zhu1997software] Zhu, Hong, Patrick AV Hall, and John HR May. 1997. “Software Unit Test Coverage and Adequacy.” Acm Computing
Surveys (Csur) 29 (4): 366–427.
• [janzen2005test] Janzen, David, and Hossein Saiedian. 2005. “Test-Driven Development Concepts, Taxonomy, and Future Direction.”
Computer 38 (9): 43–50.
• [maximilien2003assessing] Maximilien, E Michael, and Laurie Williams. 2003. “Assessing Test-Driven Development at IBM.” In Software
Engineering, 2003. Proceedings. 25th International Conference on, 564–69. IEEE.
• [williams2003test] Williams, Laurie, E Michael Maximilien, and Mladen Vouk. 2003. “Test-Driven Development as a Defect-Reduction
Practice.” In Software Reliability Engineering, 2003. ISSRE 2003. 14th International Symposium on, 34–45. IEEE.
• [kaufmann2003implications] Kaufmann, Reid, and David Janzen. 2003. “Implications of Test-Driven Development: A Pilot Study.” In
Companion of the 18th Annual ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications,
298–99. ACM.
• [edwards2003using] Edwards, Stephen H. 2003. “Using Test-Driven Development in the Classroom: Providing Students with Automatic,
Concrete Feedback on Performance.” In Proceedings of the International Conference on Education and Information Systems: Technologies
and Applications EISTA. Vol. 3. Citeseer.
Previous Next
Table of contents
Did you spot an error ? Want to give me feedback ? Here is the feedback page! ×
Newsletter:
Like what you read ? Subscribe to the newsletter.
@ my@email.com
23 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests
Practical Go Lessons
By Maximilien Andile
Copyright (c) 2023
Follow me Contents
Posts
Book
Support the author Video Tutorial
About
The author
Legal Notice
Feedback
Buy paper or digital copy
Terms and Conditions
24 of 24 02/01/2023, 02:10