0% found this document useful (0 votes)
71 views

Unit Tests - Practical Go Lessons-19

Unit testing involves testing individual units or components of software to ensure they function as intended. A unit test contains a test case with an input, expected output, and assertion to validate the actual output matches expectations. Writing unit tests provides benefits like preventing regressions, improving code quality, and serving as documentation. In Go, unit test files end with _test.go and are located in the same package as the code being tested.

Uploaded by

prsnortin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
71 views

Unit Tests - Practical Go Lessons-19

Unit testing involves testing individual units or components of software to ensure they function as intended. A unit test contains a test case with an input, expected output, and assertion to validate the actual output matches expectations. Writing unit tests provides benefits like preventing regressions, improving code quality, and serving as documentation. In Go, unit test files end with _test.go and are located in the same package as the code being tested.

Uploaded by

prsnortin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 24

Unit Tests - Practical Go Lessons https://www.practical-go-lessons.

com/chap-19-unit-tests

Chapter 19: Unit Tests

1 What will you learn in this chapter?


• What are unit tests

• How to write unit tests

• How to test your Go program with the go tool

1.1 Technical concepts covered


• Unit tests

• Test case

• Test function

• Assertion

• Test-Driven Development (TDD)

• Code coverage

2 Introduction
Here is a function :

// compute and return the total price of a hotel booking


// all amounts in input must be multiplied by 100. Currency is Dollar
// the amount returned must be divided by 100. (ex: 10132 => 101.32 $)
func totalPrice(nights, rate, cityTax uint) uint {
return nights*rate + cityTax
}

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 :

return nights*rate + cityTax

It should be replaced by this one :

return nights * (rate + cityTax)

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!

3 What is unit testing?


If we take the definition from the IEEE (Institute for Electrical and Electronics Engineers) [@institute1990ieee] unit testing is the “testing of
individual hardware or software units or groups of related units”.

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.

4 What is a test case, a test set, an assertion?


A single unit test is called a test case. A group of test cases is called a test set (or test suite).

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.

Our test case will be composed of :

• A test input. For instance : “coffee”

• An expected output : In our example, it will be the string “COFFEE”

• The actual output of our function under test

• 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

5 Why Unit testing your code?


In this section, I will go through some reasons extracted from an IEEE survey about unit testing [@runeson2006survey]:

• 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.

6 Where to put the tests?


Several languages are putting the tests into a specific directory, often called tests. In Go unit tests are living next to the code they test. Tests
are part of the package under test.

Here is the list of files from the directory src/strings :

3 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

strings package directory

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.

7 How to write a basic unit test


Let’s write our very first unit test together. We will test the package foo :

// unit-test/basic/foo/foo.go
package foo

import "fmt"

func Foo() string {


return fmt.Sprintf("Foo")
}

7.0.0.1 The test file


Let’s create a file foo_test.go in the same folder as foo.go :

// unit-test/basic/foo/foo_test.go
package foo

import "testing"

func TestFoo(t *testing.T) {

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).

7.0.0.2 Naming of a test case


A test function should be named following the same convention :

Test function signature

• 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.

7.0.0.3 The contents of a test case


Here is an example of the function Foo . The function has no argument, but it always returns the string “Foo”. If we want to unit test it, we
will assert (verify) that the return of the function is "Foo" :

// unit-test/basic/foo/foo_test.go
package foo

import "testing"

func TestFoo(t *testing.T) {


expected := "Foo"
actual := Foo()
if expected != actual {
t.Errorf("Expected %s do not match actual %s", expected, actual)
}
}

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 expected variable is the result as expected by the user.

• 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 ) :

t.Errorf("Expected %s do not match actual %s", expected, actual)

7.0.0.4 About Success and Errors


There is no method defined on the type T to signal the test success.

When the test function returns without calling a failure method, then it is interpreted as a success.

7.0.0.5 Failure signal


To signal a failure, you can use the following methods :

• 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.

• Fail : will mark the function as failing. 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

7.0.0.6 Test files


Sometimes you need to store files that will support your unit tests. For instance, an example of a configuration file, a model CSV file (for an
application that will generate files)...

Store those files into the testdata folder.

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.

To add it to your project, type the following command in your terminal :

$ 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"
)

func TestFoo(t *testing.T) {


assert.Equal(t, "Foo", Foo(), "they should be equal")
}

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

9 How to run unit test


9.1 Run the tests of a specific package
To run your unit tests, you have to use the command-line interface. Open a terminal and cd to your project directory :

$ cd go/src/gitlab.com/loir402/foo

Then run the following command :

$ go test

The following output result is displayed in the terminal :

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

9.2 Run all the tests of a project


You can run all the unit tests of your current project by launching the command :

$ go test ./...

9.3 Test failure


What is the output of a failed unit test? Here is an example. We have modified our unit test to make it crash. Instead of the string "Foo" we
are no expecting "Bar" . Consequently, the test fails.

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.

9.3.0.1 Exit Code (or exit status)


• An exit code different from 0 signals an error.

• An exit code of 0 signals NO errors

9.4 go test and go vet[subsec:Go-testvet]


When you run the go test command, the go tool will also run go vet automatically on the tested packages (source of the packages and
test files).

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!

9.5 Just compile


To compile our test without running it, you can type the following command :

$ go test -c

This will create a test binary called “packageName.test”.

10 How to write a table test


In the previous example, we tested our function against one expected result. In a real situation, you might want to test your function with
several test cases.

One approach could be to build a test function like that :

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"

func Test_totalPrice1(t *testing.T) {


// test case 1
expected := uint(0)
actual := totalPrice(0, 150, 12)
if expected != actual {
t.Errorf("Expected %d does not match actual %d", expected, actual)
}
// test case 2
expected = uint(112)
actual = totalPrice(1, 100, 12)
if expected != actual {
t.Errorf("Expected %d does not match actual %d", expected, actual)
}

// 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"

func Test_totalPrice(t *testing.T) {


type parameters struct {
nights uint
rate uint
cityTax uint
}
type testCase struct {
name string
args parameters
want uint
}
tests := []testCase{
{
name: "test 0 nights",
args: parameters{nights: 0, rate: 150, cityTax: 12},
want: 0,
},
{
name: "test 1 nights",
args: parameters{nights: 1, rate: 100, cityTax: 12},
want: 112,
},
{
name: "test 2 nights",
args: parameters{nights: 2, rate: 100, cityTax: 12},
want: 224,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := totalPrice(tt.args.nights, tt.args.rate, tt.args.cityTax); got != tt.want {
t.Errorf("totalPrice() = %v, want %v", got, tt.want)
}
})
}
}

• We create a type struct named parameters . Each field of that struct is a function parameter of the function under test

• Then we create a struct named testCase . With three fields:

◦ name : the name of the test case: a human-readable name

◦ args : the parameters to give to the function under test

◦ want : the expected value returned by the function

• A slice named tests containing elements of type testCase is created. This is here that we will manually define each test case

◦ One test case = one element of the slice.


• Then with a for loop, we iterate over the elements of the slice tests .

• At each iteration, we call the method t.Run

◦ Parameters :

▪ the test name tt.name

▪ 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.

Here is the run output of this test (successful) :

11 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

=== RUN Test_totalPrice


=== RUN Test_totalPrice/test_0_nights
=== RUN Test_totalPrice/test_1_nights
=== RUN Test_totalPrice/test_2_nights
--- PASS: Test_totalPrice (0.00s)
--- PASS: Test_totalPrice/test_0_nights (0.00s)
--- PASS: Test_totalPrice/test_1_nights (0.00s)
--- PASS: Test_totalPrice/test_2_nights (0.00s)
PASS

• 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.

11 The two go test modes


go test is a command that we can run in two different modes :

11.1 Local directory mode


This mode is triggered when you run the command :

$ 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.

11.2 Package list mode


In this mode, you can ask Go to unit test some specific packages or all the project packages.

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.

To test this behavior, launch the tests on the package strings :

$ go test strings

It will output the following :

ok strings 4.256s

You can see here that the unit test’s time is 4.256s, which is quite long.

Try to launch it again :

$ 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.

11.3.0.1 Disabling cache


Note that when you modify a test file or a source file of the package, the test result that has been cached is invalidated, and the test will be
effectively run.

To disable caching, you can use the following command flag :

12 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

$ go test strings -count=1

11.3.0.2 Environnement variables and cache


If you are using environment variables in your source files, Go will cache the test result if the environment variables set are not changing.

Let’s take an example, imagine that you are using the environment variable MYENV inside your test script :

func TestFoo(t *testing.T) {


env := os.Getenv("MYENV")
fmt.Println(env)
//..
}

The first time, when you execute the test with the environment variable set to "BAR" , then the test will run :

$ export MYENV=BAR && go test gitlab.com/loir402/foo


ok gitlab.com/loir402/foo 0.005s

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 :

$ export MYENV=CORGE && go test gitlab.com/loir402/foo


ok gitlab.com/loir402/foo 0.005s

11.3.0.3 Open files


The same mechanism is in place when your code opens a file. If you run your test for the first time, Go will cache the result. But if the file has
changed, the result is no longer cached, and the test is executed again :

func TestFoo(t *testing.T) {


d, err := ioutil.ReadFile("testdata/lol.txt")
if err != nil {
t.Errorf("impossible to open file")
}
fmt.Print(string(d))
//..
}

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.

12 Running unit test in parallel


In a big project, the number of unit tests can become very large. Running the unit tests can become time-consuming for the team.

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.

Let’s take an example:

13 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

func TestCorge1(t *testing.T) {


time.Sleep(300 * time.Millisecond)
}

func TestCorge2(t *testing.T) {


time.Sleep(300 * time.Millisecond)
}

func TestCorge3(t *testing.T) {


time.Sleep(300 * time.Millisecond)
}

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.

Let’s run those tests

$ go test

The test result is the following :

PASS
ok gitlab.com/loir402/corge 0.913s

Tests are taking 0.913seconds to run which is roughly 3 × 300ms.

Let’s make them run in parallel :

func TestCorge1(t *testing.T) {


t.Parallel()
time.Sleep(300 * time.Millisecond)
}

func TestCorge2(t *testing.T) {


t.Parallel()
time.Sleep(300 * time.Millisecond)
}

func TestCorge3(t *testing.T) {


t.Parallel()
time.Sleep(300 * time.Millisecond)
}

Here we just added

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 !

13 Advanced usage of the go test command


13.1 Arguments (-args)
You can build a test that will accept command line arguments. Those arguments can be passed to the test executable by using a flag. Let’s
take an example of a test that requires command-line arguments.

func TestArgs(t *testing.T) {


arg1 := os.Args[1]
if arg1 != "baz" {
t.Errorf("Expected baz do not match actual %s", arg1)
}
}

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 :

$ go test gitlab.com/loir402/foo -args bar

The execution result is the following :

--- FAIL: TestArgs (0.00s)


foo_test.go:24: Expected baz do not match actual bar
FAIL
FAIL gitlab.com/loir402/foo 0.005s

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.

13.2.0.1 Coverage flags


This flag will display the coverage data analysis. It’s, in my opinion, the most important flag to know. The coverage data give you a statistic
about the percentage of statements of your code that is covered by a unit test :

$ 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.

write somewhere the cover data

13.2.0.2 Test run flags


With this flag, when the first test break, all the other tests are not run. This is useful when you want to debug your code and fix problems one
by one (when they happen)

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 :

Here is an example output of the verbose mode :

=== RUN TestCorge1


=== PAUSE TestCorge1
=== RUN TestCorge2
=== PAUSE TestCorge2
=== RUN TestCorge3
=== PAUSE TestCorge3
=== CONT TestCorge1
=== CONT TestCorge3
=== CONT TestCorge2
--- PASS: TestCorge2 (0.31s)
--- PASS: TestCorge3 (0.31s)
--- PASS: TestCorge1 (0.31s)
PASS
ok gitlab.com/loir402/corge 0.311s

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

=== RUN TestCorge1


--- PASS: TestCorge1 (0.31s)
=== RUN TestCorge2
--- PASS: TestCorge2 (0.31s)
=== RUN TestCorge3
--- PASS: TestCorge3 (0.31s)
PASS
ok gitlab.com/loir402/corge 0.921s

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.

13.2.0.3 Profiling flags


The go test command line also defines specific flags to identify performances issues of your code. Those flags are covered in the specific
chapter “Profiling”

14 Code Coverage
• Is a project sufficiently tested?

• What defines a good test level?

• How to measure the testing level of a project?

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.

14.1 Cover mode set


This is the default mode. In literature, we call this mode the “statement coverage” that because it counts the percentage of executed
statements in tests [@zhu1997software].

Perfect test coverage is 100%, meaning that all the code statements have been tested.

Let’s take an example with the following code :

package testCoverage

func BazBaz(number int) int {


if number < 10 {
return number
} else {
return number
}
}

This package defines one single function. Inside this function, a conditional statement discriminates two cases. Input numbers under ten and
numbers above.

Let’s write a test :

16 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

func TestBazBaz(t *testing.T) {


expected := 3
actual := BazBaz(3)
if actual != expected {
t.Errorf("actual %d, expected %d", actual, expected)
}
}

In this unit test, we execute BazBaz with the number 3 as input.

Let’s run the test :

go test -cover
PASS
coverage: 66.7% of statements

ok go_book/testCoverage 0.005s

We only covered 66.7% of statements.

14.1.0.1 Cover Profile


To help us understand the computation go can generate a coverprofile, which is a file detailing which statements are covered.

Coverprofile HTML generated[fig:Coverprofile-HTML-generated]

To generate this file, you have to use two commands in your terminal :

$ go test -coverprofile profile

This first command will generate the profile file :

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 :

$ go tool cover -html=profile

It will create an HTML page, store it (not in your project directory) and open it on a browser.

14.1.0.2 Increase the coverage


We have a total of three statements, and two are covered: the first if statement then the first return. It means that 23 (or 66.7%) of statements
are covered.

We can increase that percentage to 100% by integrating a test of the remaining statement (the else part of our condition) :

func TestBazBaz2(t *testing.T) {


expected := 25
actual := BazBaz(25)
if actual != expected {
t.Errorf("actual %d, expected %d", actual, expected)
}
}

This will lead to a coverage of 100%. All the statements of our code are covered.

14.2 Cover mode count


The count mode is similar to the set mode. With this mode you can detect if some part of the code is covered by more tests than others.

For instance, the function :

func BazBaz(number int) int {


if number < 10 {
return number
} else {
return number
}
}

is tested by two test cases :

• One that will test an input less than 10

• One that will test an input greater than 10.

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 conditional statement is “more” tested than the other one.

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

Coverprofile Count mode[fig:Coverprofile-Count-mode]

The greener the statement is, the more it’s tested.

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.

14.3 Cover mode atomic (advanced)


The last cover mode is count. It is useful when you build concurrent programs. Internally the system will use atomic counters (instead of
simple counters). With those concurrent safe counters, the coverprofile will be more precise.

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"
)

func BazBaz(number int) int {


var waitGroup sync.WaitGroup
for i := 0; i < 100; i++ {
waitGroup.Add(1)
go concurrentTask(number, &waitGroup)
}
waitGroup.Wait()
return number
}

func concurrentTask(number int, waitGroup *sync.WaitGroup) {


useless := number + 2
fmt.Println(useless)
waitGroup.Done()
}

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.

Let’s get the coverprofile in count mode :

$ go test -coverprofile profileCount -covermode count


$ cat profileCount
mode: count
go_book/testCoverage/testCoverage.go:8.29,10.27 2 2
go_book/testCoverage/testCoverage.go:14.2,15.15 2 2
go_book/testCoverage/testCoverage.go:10.27,13.3 2 200
go_book/testCoverage/testCoverage.go:18.60,22.2 3 197

And in atomic mode :

19 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

$ go test -coverprofile profileAtomic -covermode atomic


$ cat profileAtomic
mode: atomic
go_book/testCoverage/testCoverage.go:8.29,10.27 2 2
go_book/testCoverage/testCoverage.go:14.2,15.15 2 2
go_book/testCoverage/testCoverage.go:10.27,13.3 2 200
go_book/testCoverage/testCoverage.go:18.60,22.2 3 200

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.

15 Test-Driven Development (TDD)


Test-Driven Development (or TDD) is a development method where you design the tests before actually writing the software.

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"

func TestVowelCount(t *testing.T) {


expected := uint(5)
actual := VowelCount("I love you")
if actual != expected {
t.Errorf("actual %d, expected %d", actual, expected)
}
}

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.

Let’s run our test to see what happens :

$ go test
# go_book/tdd [go_book/tdd.test]
./tdd_test.go:7:12: undefined: VowelCount
FAIL go_book/tdd [build failed]

We cannot compile; the test fails.

Now we can implement our function. We start by creating a map of vowels from the alphabet.

// unit-test/tdd/tdd.go
package tdd

var vowels = map[string]bool{


"a": true,
"e": true,
"i": true,
"o": true,
"u": true}

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

//...

func VowelCount(sentence string) uint {


var count uint
for _, char := range sentence {
if vowels[string(char)] {
count++
}
}
return count
}

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

FAIL go_book/tdd 0.005s

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.

What have to options :

1. Add the uppercase letters to our map.

2. Each letter should be converted to lowercase and then compared with the existing map.

The second solution seems to be less efficient than the first :

• 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).

Let’s add the capitalized vowels to our map :

var vowels = map[string]bool{


//...
"A": true,
"E": true,
"I": true,
"O": true,
"U": true}

Then we run our test again, and it works !

$ 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

15.0.0.2 Facts and figures


To convince you of this approach, I will focus on the fact and on real studies that have been conducted about TDD. (Those results have been
extracted from the very good article of David Janzen [@janzen2005test] that was published in 2005) :

• 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].

I hope you are convinced.

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?

4. What is an assertion? Give an example.

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?

func TestShippingCost(t *testing.T)

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.

4. What is an assertion? Give an example.

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 :

◦ It gives relative protection against regressions.

◦ Unit tests may detect bugs before they appear in production.

◦ Improve the design of your package’s API

• Unit tests live inside packages.

22 of 24 02/01/2023, 02:10
Unit Tests - Practical Go Lessons https://www.practical-go-lessons.com/chap-19-unit-tests

• Tests are written on test files.

• xxx_test.go is the conventional name of a test file.

• Test files can contain multiple test functions

• A test function can contain several test cases

• 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.

• A test function has the following signature :

func TestShippingCost(t *testing.T)

• 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 is written before the actual implementation

◦ The unit test fails first; the aim is then to make it pass.

1. Nickname: Uncle bob↩

2. This is the Big O notation↩

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

Go Module Proxies Arrays

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.

I will keep you informed about the book updates.

@ 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

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy