This post is part of a four-part series on code coverage:
- Part I: An Introduction
- Part II: A short example
- Part III: Statement coverage and some myths
- Part IV: Honest 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: Count
, Pos
and NumStmt
.
Count
: the number of times the block is runPos
: the position of the blockNumStmt
: 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-else
, switch
, for
, 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!