Table of contents

Go: basic testing and coverage

Go: basic testing and coverage

Go already has a testing module in its standard library that is ready for our use, we just need to import it and use it.

Testing preparation in go

For the tests to be carried out we need:

A file ending in _test.go * A file ending in _test.go * Run the command go test.

  • Run the go test command
├── go.mod
├── main.go
└── testing
    ├── main.go
    └── main_test.go

1 directory, 5 files

Consider that, if you are going to assign a name to your package, you should never name it testing. Why? If you do, go will confuse its testing package with yours, returning those incorrect results.

To create the tests, inside the testing/main_test.go file, we need a function that receives as argument our testing package with the destructuring character.

We will compare the result using an if, or whatever we want and, if the test fails, we will call the Errorf method of the testing module.

package main

import "testing"

func TestDivision(t *testing.T) {
    total := Division(10, 2)
    if total != 5 {
        t.Errorf("División incorrecta, obtuvimos %d pero se esperaba %d", total, 5)
    }
}

It is not necessary that the functions to be tested are inside the testing file, in this case I placed them inside testing/main.go.

package main

func Division(a int, b int) int {
    return a / b
}

Execute tests

To run the tests we need to find ourselves inside the directory where our files ending in _test.go are located and run the go test command. If the test passes we will get the PASS message.

cd testing/
go test

PASS
ok main/testing 0.001s

On the other hand, if the tests fail, the word FAIL will be printed on the screen:

--- FAIL: TestDivision (0.00s)
    main_test.go:14: División incorrecta, obtuvimos 12 pero se esperaba 5
FAIL
exit status 1
FAIL main/testing 0.001s

Case management with tables

In the above example we used one function to test one case, however, if we needed to test multiple cases, we would need a function for each test, quite tedious, right?

To avoid filling up with functions, developers use an array composed of structs, where each struct represents a case to be tested. You can think of the array of structs as a table, where each row is a case and each column is a data type to be tested.

In this case, each struct in our array consists of three integers; the first two represent the arguments, while the last one is the result.

tables := []struct {
    	x int
    	y int
    	r int
    }{
    	{100, 10, 10}, // 100 / 10 = 10
    	{200, 20, 10}, // 200 / 20 = 10
    	{300, 30, 10},
    	{1000, 100, 10},
    }

I’m sure you’ve noticed that we’re not covering division by zero, but leave it at that for now.

Now that we have our array of structs, we will iterate over each of its elements using go’s range function. This way we will cover each case.

for _, table := range tables {
    	total := Division(table.x, table.y)
    	if total != table.r {
    		t.Errorf("División de %d entre %d incorrecta, obtuvimos: %d, pero el resultado es: %d.", table.x, table.y, total, table.r)
    	}
    }

If everything went well, we will pass all the tests.

Coverage

Coverage is already part of the code in go, so we do not need external libraries. If you don’t know what Coverage is, think of it as the percentage of your code that is tested. If all your code goes through the tests you will have a coverage of 100%, if only half of it goes through the tests the coverage will be 50%. Previously I talked about coverage in my entry unittest in Python

To calculate the coverage, simply add the -cover flag to the go test command.

go test -cover

PASS
coverage: 100.0% of statements
ok _/home/eduardo/Programacion/goTesting/testing 0.002s

As our function is tiny, we obtain a result of 100%, without breaking down, from coverage

Export coverage results

We can send all the raw data from our coverage test to an external file with the -coverprofile flag.

go test -coverprofile=coverage.out

mode: set
/home/eduardo/Programacion/goTesting/testing/main.go:3.33,5.2 1 1

This file, named coverage.out, which was generated, is a file containing raw data and will be needed to visualize the results in a more detailed way.

Viewing results with go tool

To summarize in a more readable way the information of the file containing our coverage test, we will use the tool command, accompanied by the -func flag, followed by the file name. This will return a broken down coverage result.

go tool cover -func=coverage.out

/home/eduardo/Programacion/goTesting/testing/main.go:3: Division 100.0%
total:                                                  (statements)    100.0%

Go also allows us to visualize the coverage in HTML format, with colors, directly in our browser. For this we use the -html option, followed by the file with the coverage data.

When the command is executed, a browser tab will open and show the tested results in green and the untested results in red.

go tool cover -html=coverage.out

Coverage en go

Full html coverage in go

If we decide to modify our function to handle the division by zero cases, and run the coverage tests again, we will get a different scheme than before. Now a section of code not covered by the tests appears in red and our coverage dropped to 50%.

Screenshot of coverage en go

Incomplete html coverage in go

This is the end of this super short entry about go testing. For the next entry I will talk a little bit about profiling and I will finish the basic go entries to write again about Python.

Other resources on testing

Eduardo Zepeda
Web developer and GNU/Linux enthusiast always learning something new. I believe in choosing the right tool for the job and that simplicity is the ultimate sophistication. I'm under the impression that being perfect is the enemy of getting things done. I also believe in the goodnesses of cryptocurrencies outside of monetary speculation.
Read more