You know that code that is tricky to unit test but easy to make an integration test for? And you track code coverage?
Well, the upcoming Go 1.20 release adds support for collecting code coverage from integration tests.
Go 1.20 has a new trick and can build binaries ready to collect coverage during integration tests. If you’re like me, you want to know how to combine code coverage from unit and integration tests. Let’s walk through that.
Note: The new coverage reports are in a binary format. The typical
go test -coverprofile=c.out ./...
produces a text format. These two formats are not compatible. This post demonstrates Go 1.20’s new tooling to merge and convert the binary format into the text format.
Create a sample Go program
To show off collecting combined unit and integration coverage reports, we’ll start by creating a Go program to exercise.
If you’d prefer, you can clone the demo repository instead: go-combined-unit-integration-coverage-demo.
Our Go program will feature a CLI that takes two numbers and prints the sum. Our project structure will look like this:
.
├── cmd
│ └── add
│ └── main.go
├── go.mod
└── internal
└── calculator
├── calculator.go
└── calculator_test.go
We’ll create an internal calculator
package with an Add
and a Multiply
function. We’ll create unit tests for
the calculator
package to have 100% unit test code coverage. Later, we’ll execute our CLI twice to collect code
coverage of our main
function.
Create a new directory, Go module, and new files by running:
|
|
Populate internal/calculator/calculator.go
with:
|
|
and add unit tests in internal/calculator/calculator_test.go
:
|
|
Finally, we’ll create our main
package in cmd/add/main.go
:
|
|
As a sanity check, unit tests should pass, and we can build our program by running the following:
|
|
Build a binary for coverage collection
With Go 1.20, we can create a variation of our program with coverage collection enabled.
Build a binary with coverage reporting enabled by running:
|
|
Run the binary to collect coverage
We’ve built our binary ready to report coverage. Now we want to run it.
Start by creating a directory coverage/int
for our coverage reports for integration tests.
|
|
We can still run our program like usual:
|
|
But we’ll see a warning printed:
warning: GOCOVERDIR not set, no coverage data emitted
If we then set GOCOVERDIR
and run the following command:
|
|
We’ll have binary coverage reports in ./coverage/int
now.
We can execute our binary multiple times to exercise different code paths. This time let’s run our program in a manner that will error.
Please run the following command and notice we provide no arguments to our ./bin/add
.
|
|
The command will fail, and that’s okay. Coverage reports will continue appear in in the ./coverage/int
directory.
At this point, we can see code coverage from our integration tests (executing
./bin/add
twice) by running:
|
|
and we’ll see the following output:
combined-coverage-demo/cmd/add coverage: 77.8% of statements
combined-coverage-demo/internal/calculator coverage: 50.0% of statements
Run unit tests to collect coverage
Before Go 1.20, we’d collect code coverage profiles by running:
|
|
The generated cover profile is in a text format and incompatible with the new binary format. There’s a way to merge multiple binary reports (go tool covdata merge
), but there isn’t currently built-in tooling to combine coverage profiles.
Fortunately, with Go 1.20, there’s a way to instruct go test
to create binary coverage reports too.
Create a new directory to store binary coverage for unit tests:
|
|
Then run the following command to generate coverage from unit tests:
|
|
We can see unit test code coverage by running:
|
|
and see our coverage on internal/calculator
:
combined-coverage-demo/internal/calculator coverage: 100.0% of statements
Note:
-args -test.gocoverdir=...
can be read in the proposal for cmd/cover: extend coverage testing to include applications
Retrieve total coverage
So far, we’ve used go tool covdata percent
to display code coverage separately from
unit and from integration tests.
Fortunately, go tool covdata percent
supports multiple directories. So we can
run:
|
|
and see the combined coverage report:
combined-coverage-demo/cmd/add coverage: 77.8% of statements
combined-coverage-demo/internal/calculator coverage: 100.0% of statements
Convert total coverage to cover profile
So, we can see combined coverage from binary reports. But so much tooling and reporting already exist around the previous text format.
Compatbility with existing tools has been thought about and go tool covdata
supports converting
binary reports into a cover profile.
Covert our binary reports to a text profile by running:
|
|
And finally, we can view our total coverage again by running:
|
|
to see:
combined-coverage-demo/cmd/add/main.go:12: main 77.8%
combined-coverage-demo/internal/calculator/calculator.go:3: Add 100.0%
combined-coverage-demo/internal/calculator/calculator.go:7: Multiply 100.0%
total: (statements) 81.8%
Integration code coverage will be a big deal for the Go community. Reporting complete coverage from unit and integration tests will go a long way in helping teams have confidence in their tests.
Do you have other interesting use cases for this? Please feel free to reach out on Twitter, LinkedIn, or GitHub. Let me know!