Golang gotchas #5 code coverage test

devops terminal
5 min readMar 27, 2023
Photo by Markus Spiske on Unsplash

We probably heard of unit testing in Go, but do we ever thought of running a code coverage for our code source? Maybe… the question is why do we need a code coverage?

Goals of code coverage

Traditionally, we dive into the code development and created lots of code pieces which is not battled tested. After a while, we started to bring in the concept of unit testing; which means we start to develop test cases before coding the pieces, the advantage of this approach is that most of the code pieces would be battled tested before deployment.

Though unit testing is a great technique, but still it failed to prevent us from writing code pieces that was NEVER touched. Think in this way, we are supposed to write a query module for a library system. We thought that users would love to query books based on number of pages and hence created a function: QueryByNumOfPages(pages int), and of course we added a unit test for this function. Everything seems fine and smooth. But the fact is… the users actually don’t need this function / behaviour at all, hence eventually this function (exposed as api) is never called by the frontend application (website or mobile phone apps). Hm… now the question is how do we ever know such function is an orphan in the system? Obviously unit test is not the answer.

Code coverage is a technique to run unit tests and match through the original code piece whether all conditions and branches are covered within the test cases. The advantage of this operation is that orphan functions would be discovered and hence lead to better maintainability (by removing the function or re-evaluate the need for this function). Likewise some functions are being tested on only the main branch logics, hence it would be essential to cover the remaining branches to make sure it is battle tested for all conditions (branches).

A sample Go project

This is the project’s structure as reference:

2 packages contain unit test files (internal/strings/test and pkg/math/test).

A note on package “internal”, sometimes we do have code pieces in which we would need to sharable within the same project BUT not to the outside world, in certain languages this is known as a “protected” package. In Go, the internal package would be re-usable with the project just like ordinary packages, however internal packages act like a private package and are not accessible by external parties. Hence in case you need some utility functions for the project but do not want to expose to the others, “internal” packages are the right place.

code coverage steps

Cool, once we have a project with multiple packages for running test cases, let’s try to run the code coverage~

go test -cover

oops… error~ “no Go files in /go_blogs/05_codecoverage”; this simply means we need to provide some paths for accessing *.go files.

go test -cover ./…

cool~ we did it~ Is it??? Based on the logs on the console, seems we did found some test cases and it did take some milliseconds to run through, however… check the coverage sentence — [ no statements ].

“no statements” means nothing was involved in the code coverage exercise. Interestingly, we need to state which package(s) we are interested in code coverage, this makes sense since every Go program involves calling lots of built-in libraries, hence if by default all packages are being covered… then we would get back lots of statistics which is not what we want to focus at~

go test -cover -coverpkg=./pkg/… ./…

This is more like it~ The “-coverpkg” setting tells where the compiler should start the coverage, in this case any packages starting from /pkg/.

Based on the logs, we know that there are 2 packages involving unit tests and somehow the test cases under the /pkg/math/test have a coverage statistics now — [ 100% of statements in ./pkg/… ], but then the coverage statistics for internal package is still 0.00%

let’s reshape the coverage a bit:

go test -cover -coverpkg=./pkg/…,./internal/… ./…

cool~ now both packages have a coverage~ Do note that the coverage is 50% here because we do stated 2 packages (internal and pkg) for coverage and when we are scanning the pkg packages of course only 1 of the stated package paths are involved (in this case ./pkg/… is involved and ./internal/… isn’t and so only 50%)

Nice nice but what if we want to know exactly which function or branch is covered in details? (we only have a number in the previous coverage command and lack of details)

Let’s create a html report then~ The report generation involves 2 steps:

create a stats file by:

go test -coverprofile=coverage.out -coverpkg=./pkg/…,./internal/… ./…

replace “-cover” with “-coverprofile=xxx.out” and the xxx.out file would be generated with statistics meta data.

Next is to generate the report based on the xxx.out:

go tool cover -html=coverage.out -o coverage.html

by now the report named coverage.html should be created, open in a browser and we should see something like this

showing the coverage results
there are 2 unit tests involved and hence pickable in the combo-box

closings

Cool~ We have just finished an adventure on code coverage.

  • code coverage is built-in under Go
  • the -coverpkg configuration MUST be provided in order to run coverage on certain packages (missing this setting would end up 0.00% code coverage)
  • we can generate an html report based on the coverage statistics and is recommended

One last thing about code coverage is… Try to achieve a high % of coverage but not saying that 100% is a MUST for every scenario.

Take an example, we add in error checking in Go but some error scenarios are pretty rare, which means it is hard to cover this branch unless we deliberately created faulty conditions to make it happen. For simple and small libraries, it is easy to achieve 100% coverage but for complex systems and modules, it would be getting more and more challenging, hence an acceptable % of coverage might be more reasonable and should be agreed by the development team beforehand.

--

--

devops terminal

a java / golang / flutter developer, a big data scientist, a father :)