Code Coverage – Part II: A short example

This post is part of a four-part series on code coverage:


Golang is an excellent language that supports great tooling, by design (read more here). There are other languages that support coverage analysis natively, but for the purpose of understanding code coverage, seeing how it works in Golang will suffice.

Let’s use the following code as an example. It creates a HTTP server that provides album information from a hard-coded slice.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
	{ID: "1", Title: "Superman (It's Not Easy)", Artist: "Five for Fighting", Price: 49.99},
	{ID: "2", Title: "100 Years", Artist: "Five for Fighting", Price: 59.99},
	{ID: "3", Title: "The Riddle", Artist: "Five for Fighting", Price: 69.99},
}

func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.GET("/albums/:id", getAlbumByID)

	router.Run(":11111")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, albums)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
	id := c.Param("id")

	// Loop through the list of albums, looking for
	// an album whose ID value matches the parameter.
	for _, a := range albums {
		if a.ID == id {
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

If you have tests in the same directory, simply run go test -cover . and you can obtain coverage information for your tests for this server. If your tests are deployed separately from the server being tested, you can use other tools like https://github.com/qiniu/goc/ to obtain coverage information.

Behind the scenes

First, Go creates temporary copy of your project with a few modifications. We can use go tool cover -mode=count to see what this copy looks like.

package main

import (
        "net/http"

        "github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
        ID     string  `json:"id"`
        Title  string  `json:"title"`
        Artist string  `json:"artist"`
        Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
        {ID: "1", Title: "Superman (It's Not Easy)", Artist: "Five for Fighting", Price: 49.99},
        {ID: "2", Title: "100 Years", Artist: "Five for Fighting", Price: 59.99},
        {ID: "3", Title: "The Riddle", Artist: "Five for Fighting", Price: 69.99},
}

func main() {GoCover.Count[0]++;
        router := gin.Default()
        router.GET("/albums", getAlbums)
        router.GET("/albums/:id", getAlbumByID)

        router.Run(":11111")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {GoCover.Count[1]++;
        c.IndentedJSON(http.StatusOK, albums)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {GoCover.Count[2]++;
        id := c.Param("id")

        // Loop through the list of albums, looking for
        // an album whose ID value matches the parameter.
        for _, a := range albums {GoCover.Count[4]++;
                if a.ID == id {GoCover.Count[5]++;
                        c.IndentedJSON(http.StatusOK, a)
                        return
                }
        }
        GoCover.Count[3]++;c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

var GoCover = struct {
        Count     [6]uint32
        Pos       [3 * 6]uint32
        NumStmt   [6]uint16
} {
        Pos: [3 * 6]uint32{
                24, 30, 0x2000d, // [0]
                33, 35, 0x20020, // [1]
                39, 44, 0x1b0023, // [2]
                50, 50, 0x4a0002, // [3]
                44, 45, 0x11001b, // [4]
                45, 48, 0x40011, // [5]
        },
        NumStmt: [6]uint16{
                4, // 0
                1, // 1
                2, // 2
                1, // 3
                1, // 4
                2, // 5
        },
}

Notice the extra statements like GoCover.Count[x]++? These are added by the Go tooling system, along with another file containing the GoCover struct appended at the end of the file.

These GoCover.Count[x]++ statements demarcate instrument blocks. Instrument blocks are bounded by curly braces ({...}) – they try to follow the separation of branches in the program as closely as they are written in the source. The GoCover struct indicates 3 pieces of information for every instrument block: CountPos and NumStmt.

  • Count: the number of times the block is run
  • Pos: the position of the block
  • NumStmt: the number of statements within the block

Since we used -mode=count, the GoCover struct increments the Count field every time the block is run. Another mode is -mode=set which sets the Count to 1 every time the block is run, so we can get simpler coverage information. It would be rewarding to spend some time to get the cover profile for various logic structures (if-elseswitchfor, a combination of these, …) and see how the instrument blocks are demarcated.

And that’s it! Pretty straightforward, right?

Wrapping up

While this method doesn’t fully mirror the branches in the binary, it gets us more than half the way to better understanding which parts of our tests cover the code. Since the coverage is derived directly from the source, we can obtain other useful coverage information, such as:

  • statement coverage
  • function coverage
  • package coverage

To better visualise the coverage details, checkout  Cobertura and https://github.com/AlekSi/gocov-xml

If certain functions or packages have 0% coverage, it’s pretty easy to conclude that none of our tests touch that part of the code yet. But what about those parts with higher coverage? How would it compare with the branch coverage? Let’s dive into that in the next post!